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
 * Extends the IMS Tool provider library for the LTI enrolment.
19
 *
20
 * @package    enrol_lti
21
 * @copyright  2016 John Okely <john@moodle.com>
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace enrol_lti;
26
 
27
defined('MOODLE_INTERNAL') || die;
28
 
29
use context;
30
use core\notification;
31
use core_user;
32
use enrol_lti\output\registration;
33
use html_writer;
34
use IMSGlobal\LTI\Profile\Item;
35
use IMSGlobal\LTI\Profile\Message;
36
use IMSGlobal\LTI\Profile\ResourceHandler;
37
use IMSGlobal\LTI\Profile\ServiceDefinition;
38
use IMSGlobal\LTI\ToolProvider\ToolProvider;
39
use moodle_exception;
40
use moodle_url;
41
use stdClass;
42
 
43
require_once($CFG->dirroot . '/user/lib.php');
44
 
45
/**
46
 * Extends the IMS Tool provider library for the LTI enrolment.
47
 *
48
 * @package    enrol_lti
49
 * @copyright  2016 John Okely <john@moodle.com>
50
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
51
 */
52
class tool_provider extends ToolProvider {
53
 
54
    /**
55
     * @var stdClass $tool The object representing the enrol instance providing this LTI tool
56
     */
57
    protected $tool;
58
 
59
    /**
60
     * Remove $this->baseUrl (wwwroot) from a given url string and return it.
61
     *
62
     * @param string $url The url from which to remove the base url
63
     * @return string|null A string of the relative path to the url, or null if it couldn't be determined.
64
     */
65
    protected function strip_base_url($url) {
66
        if (substr($url, 0, strlen($this->baseUrl)) == $this->baseUrl) {
67
            return substr($url, strlen($this->baseUrl));
68
        }
69
        return null;
70
    }
71
 
72
    /**
73
     * Create a new instance of tool_provider to handle all the LTI tool provider interactions.
74
     *
75
     * @param int $toolid The id of the tool to be provided.
76
     */
77
    public function __construct($toolid) {
78
        global $CFG, $SITE;
79
 
80
        $token = helper::generate_proxy_token($toolid);
81
 
82
        $tool = helper::get_lti_tool($toolid);
83
        $this->tool = $tool;
84
 
85
        $dataconnector = new data_connector();
86
        parent::__construct($dataconnector);
87
 
88
        // Override debugMode and set to the configured value.
89
        $this->debugMode = $CFG->debugdeveloper;
90
 
91
        $this->baseUrl = $CFG->wwwroot;
92
        $toolpath = helper::get_launch_url($toolid);
93
        $toolpath = $this->strip_base_url($toolpath);
94
 
95
        $vendorid = $SITE->shortname;
96
        $vendorname = $SITE->fullname;
97
        $vendordescription = trim(html_to_text($SITE->summary));
98
        $this->vendor = new Item($vendorid, $vendorname, $vendordescription, $CFG->wwwroot);
99
 
100
        $name = helper::get_name($tool);
101
        $description = helper::get_description($tool);
102
        $icon = helper::get_icon($tool)->out();
103
        $icon = $this->strip_base_url($icon);
104
 
105
        $this->product = new Item(
106
            $token,
107
            $name,
108
            $description,
109
            helper::get_proxy_url($tool),
110
            '1.0'
111
        );
112
 
113
        $requiredmessages = [
114
            new Message(
115
                'basic-lti-launch-request',
116
                $toolpath,
117
                [
118
                   'Context.id',
119
                   'CourseSection.title',
120
                   'CourseSection.label',
121
                   'CourseSection.sourcedId',
122
                   'CourseSection.longDescription',
123
                   'CourseSection.timeFrame.begin',
124
                   'ResourceLink.id',
125
                   'ResourceLink.title',
126
                   'ResourceLink.description',
127
                   'User.id',
128
                   'User.username',
129
                   'Person.name.full',
130
                   'Person.name.given',
131
                   'Person.name.family',
132
                   'Person.email.primary',
133
                   'Person.sourcedId',
134
                   'Person.name.middle',
135
                   'Person.address.street1',
136
                   'Person.address.locality',
137
                   'Person.address.country',
138
                   'Person.address.timezone',
139
                   'Person.phone.primary',
140
                   'Person.phone.mobile',
141
                   'Person.webaddress',
142
                   'Membership.role',
143
                   'Result.sourcedId',
144
                   'Result.autocreate'
145
                ]
146
            )
147
        ];
148
        $optionalmessages = [
149
        ];
150
 
151
        $this->resourceHandlers[] = new ResourceHandler(
152
             new Item(
153
                 $token,
154
                 helper::get_name($tool),
155
                 $description
156
             ),
157
             $icon,
158
             $requiredmessages,
159
             $optionalmessages
160
        );
161
 
162
        $this->requiredServices[] = new ServiceDefinition(['application/vnd.ims.lti.v2.toolproxy+json'], ['POST']);
163
        $this->requiredServices[] = new ServiceDefinition(['application/vnd.ims.lis.v2.membershipcontainer+json'], ['GET']);
164
    }
165
 
166
    /**
167
     * Override onError for custom error handling.
168
     * @return void
169
     */
170
    protected function onError() {
171
        global $OUTPUT;
172
 
173
        $message = $this->message;
174
        if ($this->debugMode && !empty($this->reason)) {
175
            $message = $this->reason;
176
        }
177
 
178
        // Display the error message from the provider's side if the consumer has not specified a URL to pass the error to.
179
        if (empty($this->returnUrl)) {
180
            $this->errorOutput = $OUTPUT->notification(get_string('failedrequest', 'enrol_lti', ['reason' => $message]), 'error');
181
        }
182
    }
183
 
184
    /**
185
     * Override onLaunch with tool logic.
186
     * @return void
187
     */
188
    protected function onLaunch() {
189
        global $DB, $SESSION, $CFG;
190
 
191
        // Check for valid consumer.
192
        if (empty($this->consumer) || $this->dataConnector->loadToolConsumer($this->consumer) === false) {
193
            $this->ok = false;
194
            $this->message = get_string('invalidtoolconsumer', 'enrol_lti');
195
            return;
196
        }
197
 
198
        $url = helper::get_launch_url($this->tool->id);
199
        // If a tool proxy has been stored for the current consumer trying to access a tool,
200
        // check that the tool is being launched from the correct url.
201
        $correctlaunchurl = false;
202
        if (!empty($this->consumer->toolProxy)) {
203
            $proxy = json_decode($this->consumer->toolProxy);
204
            $handlers = $proxy->tool_profile->resource_handler;
205
            foreach ($handlers as $handler) {
206
                foreach ($handler->message as $message) {
207
                    $handlerurl = new moodle_url($message->path);
208
                    $fullpath = $handlerurl->out(false);
209
                    if ($message->message_type == "basic-lti-launch-request" && $fullpath == $url) {
210
                        $correctlaunchurl = true;
211
                        break 2;
212
                    }
213
                }
214
            }
215
        } else if ($this->tool->secret == $this->consumer->secret) {
216
            // Test if the LTI1 secret for this tool is being used. Then we know the correct tool is being launched.
217
            $correctlaunchurl = true;
218
        }
219
        if (!$correctlaunchurl) {
220
            $this->ok = false;
221
            $this->message = get_string('invalidrequest', 'enrol_lti');
222
            return;
223
        }
224
 
225
        // Before we do anything check that the context is valid.
226
        $tool = $this->tool;
227
        $context = context::instance_by_id($tool->contextid);
228
 
229
        // Set the user data.
230
        $user = new stdClass();
231
        $user->username = helper::create_username($this->consumer->getKey(), $this->user->ltiUserId);
232
        if (!empty($this->user->firstname)) {
233
            $user->firstname = $this->user->firstname;
234
        } else {
235
            $user->firstname = $this->user->getRecordId();
236
        }
237
        if (!empty($this->user->lastname)) {
238
            $user->lastname = $this->user->lastname;
239
        } else {
240
            $user->lastname = $this->tool->contextid;
241
        }
242
 
243
        $user->email = core_user::clean_field($this->user->email, 'email');
244
 
245
        // Get the user data from the LTI consumer.
246
        $user = helper::assign_user_tool_data($tool, $user);
247
 
248
        // Check if the user exists.
249
        if (!$dbuser = $DB->get_record('user', ['username' => $user->username, 'deleted' => 0])) {
250
            // If the email was stripped/not set then fill it with a default one. This
251
            // stops the user from being redirected to edit their profile page.
252
            if (empty($user->email)) {
253
                $user->email = $user->username .  "@example.com";
254
            }
255
 
256
            $user->auth = 'lti';
257
            $user->id = \user_create_user($user);
258
 
259
            // Get the updated user record.
260
            $user = $DB->get_record('user', ['id' => $user->id]);
261
        } else {
262
            if (helper::user_match($user, $dbuser)) {
263
                $user = $dbuser;
264
            } else {
265
                // If email is empty remove it, so we don't update the user with an empty email.
266
                if (empty($user->email)) {
267
                    unset($user->email);
268
                }
269
 
270
                $user->id = $dbuser->id;
271
                \user_update_user($user);
272
 
273
                // Get the updated user record.
274
                $user = $DB->get_record('user', ['id' => $user->id]);
275
            }
276
        }
277
 
278
        // Update user image.
279
        if (isset($this->user) && isset($this->user->image) && !empty($this->user->image)) {
280
            $image = $this->user->image;
281
        } else {
282
            // Use custom_user_image parameter as a fallback.
283
            $image = $this->resourceLink->getSetting('custom_user_image');
284
        }
285
 
286
        // Check if there is an image to process.
287
        if ($image) {
288
            helper::update_user_profile_image($user->id, $image);
289
        }
290
 
291
        // Check if we need to force the page layout to embedded.
292
        $isforceembed = $this->resourceLink->getSetting('custom_force_embed') == 1;
293
 
294
        // Check if we are an instructor.
295
        $isinstructor = $this->user->isStaff() || $this->user->isAdmin();
296
 
297
        if ($context->contextlevel == CONTEXT_COURSE) {
298
            $courseid = $context->instanceid;
299
            $urltogo = new moodle_url('/course/view.php', ['id' => $courseid]);
300
 
301
        } else if ($context->contextlevel == CONTEXT_MODULE) {
302
            $cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST);
303
            $urltogo = new moodle_url('/mod/' . $cm->modname . '/view.php', ['id' => $cm->id]);
304
 
305
            // If we are a student in the course module context we do not want to display blocks.
306
            if (!$isforceembed && !$isinstructor) {
307
                $isforceembed = true;
308
            }
309
        } else {
310
            throw new \moodle_exception('invalidcontext');
311
            exit();
312
        }
313
 
314
        // Force page layout to embedded if necessary.
315
        if ($isforceembed) {
316
            $SESSION->forcepagelayout = 'embedded';
317
        } else {
318
            // May still be set from previous session, so unset it.
319
            unset($SESSION->forcepagelayout);
320
        }
321
 
322
        // Enrol the user in the course with no role.
323
        $result = helper::enrol_user($tool, $user->id);
324
 
325
        // Display an error, if there is one.
326
        if ($result !== helper::ENROLMENT_SUCCESSFUL) {
327
            throw new \moodle_exception($result, 'enrol_lti');
328
            exit();
329
        }
330
 
331
        // Give the user the role in the given context.
332
        $roleid = $isinstructor ? $tool->roleinstructor : $tool->rolelearner;
333
        role_assign($roleid, $user->id, $tool->contextid);
334
 
335
        // Login user.
336
        $sourceid = $this->user->ltiResultSourcedId;
337
        $serviceurl = $this->resourceLink->getSetting('lis_outcome_service_url');
338
 
339
        // Check if we have recorded this user before.
340
        if ($userlog = $DB->get_record('enrol_lti_users', ['toolid' => $tool->id, 'userid' => $user->id])) {
341
            if ($userlog->sourceid != $sourceid) {
342
                $userlog->sourceid = $sourceid;
343
            }
344
            if ($userlog->serviceurl != $serviceurl) {
345
                $userlog->serviceurl = $serviceurl;
346
            }
347
            if (empty($userlog->consumersecret)) {
348
                $userlog->consumersecret = $this->consumer->secret;
349
            }
350
            $userlog->lastaccess = time();
351
            $DB->update_record('enrol_lti_users', $userlog);
352
        } else {
353
            // Add the user details so we can use it later when syncing grades and members.
354
            $userlog = new stdClass();
355
            $userlog->userid = $user->id;
356
            $userlog->toolid = $tool->id;
357
            $userlog->serviceurl = $serviceurl;
358
            $userlog->sourceid = $sourceid;
359
            $userlog->consumerkey = $this->consumer->getKey();
360
            $userlog->consumersecret = $this->consumer->secret;
361
            $userlog->lastgrade = 0;
362
            $userlog->lastaccess = time();
363
            $userlog->timecreated = time();
364
            $userlog->membershipsurl = $this->resourceLink->getSetting('ext_ims_lis_memberships_url');
365
            $userlog->membershipsid = $this->resourceLink->getSetting('ext_ims_lis_memberships_id');
366
 
367
            $DB->insert_record('enrol_lti_users', $userlog);
368
        }
369
 
370
        // Finalise the user log in.
371
        complete_user_login($user);
372
 
373
        // Everything's good. Set appropriate OK flag and message values.
374
        $this->ok = true;
375
        $this->message = get_string('success');
376
 
377
        if (empty($CFG->allowframembedding)) {
378
            // Provide an alternative link.
379
            $stropentool = get_string('opentool', 'enrol_lti');
380
            echo html_writer::tag('p', get_string('frameembeddingnotenabled', 'enrol_lti'));
381
            echo html_writer::link($urltogo, $stropentool, ['target' => '_blank']);
382
        } else {
383
            // All done, redirect the user to where they want to go.
384
            redirect($urltogo);
385
        }
386
    }
387
 
388
    /**
389
     * Override onRegister with registration code.
390
     */
391
    protected function onRegister() {
392
        global $PAGE;
393
 
394
        if (empty($this->consumer)) {
395
            $this->ok = false;
396
            $this->message = get_string('invalidtoolconsumer', 'enrol_lti');
397
            return;
398
        }
399
 
400
        if (empty($this->returnUrl)) {
401
            $this->ok = false;
402
            $this->message = get_string('returnurlnotset', 'enrol_lti');
403
            return;
404
        }
405
 
406
        if ($this->doToolProxyService()) {
407
            // Map tool consumer and published tool, if necessary.
408
            $this->map_tool_to_consumer();
409
 
410
            // Indicate successful processing in message.
411
            $this->message = get_string('successfulregistration', 'enrol_lti');
412
 
413
            // Prepare response.
414
            $returnurl = new moodle_url($this->returnUrl);
415
            $returnurl->param('lti_msg', get_string("successfulregistration", "enrol_lti"));
416
            $returnurl->param('status', 'success');
417
            $guid = $this->consumer->getKey();
418
            $returnurl->param('tool_proxy_guid', $guid);
419
 
420
            $returnurlout = $returnurl->out(false);
421
 
422
            $registration = new registration($returnurlout);
423
            $output = $PAGE->get_renderer('enrol_lti');
424
            echo $output->render($registration);
425
 
426
        } else {
427
            // Tell the consumer that the registration failed.
428
            $this->ok = false;
429
            $this->message = get_string('couldnotestablishproxy', 'enrol_lti');
430
        }
431
    }
432
 
433
    /**
434
     * Performs mapping of the tool consumer to a published tool.
435
     *
436
     * @throws moodle_exception
437
     */
438
    public function map_tool_to_consumer() {
439
        global $DB;
440
 
441
        if (empty($this->consumer)) {
442
            throw new moodle_exception('invalidtoolconsumer', 'enrol_lti');
443
        }
444
 
445
        // Map the consumer to the tool.
446
        $mappingparams = [
447
            'toolid' => $this->tool->id,
448
            'consumerid' => $this->consumer->getRecordId()
449
        ];
450
        $mappingexists = $DB->record_exists('enrol_lti_tool_consumer_map', $mappingparams);
451
        if (!$mappingexists) {
452
            $DB->insert_record('enrol_lti_tool_consumer_map', (object) $mappingparams);
453
        }
454
    }
455
}