Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | 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
// This file is part of BasicLTI4Moodle
18
//
19
// BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability)
20
// consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web
21
// based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI
22
// specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS
23
// are already supporting or going to support BasicLTI. This project Implements the consumer
24
// for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas.
25
// BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem
26
// at the GESSI research group at UPC.
27
// SimpleLTI consumer for Moodle is an implementation of the early specification of LTI
28
// by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a
29
// Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier.
30
//
31
// BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis
32
// of the Universitat Politecnica de Catalunya http://www.upc.edu
33
// Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu.
34
 
35
/**
36
 * This file contains the library of functions and constants for the lti module
37
 *
38
 * @package mod_lti
39
 * @copyright  2009 Marc Alier, Jordi Piguillem, Nikolas Galanis
40
 *  marc.alier@upc.edu
41
 * @copyright  2009 Universitat Politecnica de Catalunya http://www.upc.edu
42
 * @author     Marc Alier
43
 * @author     Jordi Piguillem
44
 * @author     Nikolas Galanis
45
 * @author     Chris Scribner
46
 * @copyright  2015 Vital Source Technologies http://vitalsource.com
47
 * @author     Stephen Vickers
48
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
49
 */
50
 
51
defined('MOODLE_INTERNAL') || die;
52
 
53
// TODO: Switch to core oauthlib once implemented - MDL-30149.
54
use mod_lti\helper;
55
use moodle\mod\lti as lti;
56
use Firebase\JWT\JWT;
57
use Firebase\JWT\JWK;
58
use Firebase\JWT\Key;
59
use mod_lti\local\ltiopenid\jwks_helper;
60
use mod_lti\local\ltiopenid\registration_helper;
61
 
62
global $CFG;
63
require_once($CFG->dirroot.'/mod/lti/OAuth.php');
64
require_once($CFG->libdir.'/weblib.php');
65
require_once($CFG->dirroot . '/course/modlib.php');
66
require_once($CFG->dirroot . '/mod/lti/TrivialStore.php');
67
 
68
define('LTI_URL_DOMAIN_REGEX', '/(?:https?:\/\/)?(?:www\.)?([^\/]+)(?:\/|$)/i');
69
 
70
define('LTI_LAUNCH_CONTAINER_DEFAULT', 1);
71
define('LTI_LAUNCH_CONTAINER_EMBED', 2);
72
define('LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS', 3);
73
define('LTI_LAUNCH_CONTAINER_WINDOW', 4);
74
define('LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW', 5);
75
 
76
define('LTI_TOOL_STATE_ANY', 0);
77
define('LTI_TOOL_STATE_CONFIGURED', 1);
78
define('LTI_TOOL_STATE_PENDING', 2);
79
define('LTI_TOOL_STATE_REJECTED', 3);
80
define('LTI_TOOL_PROXY_TAB', 4);
81
 
82
define('LTI_TOOL_PROXY_STATE_CONFIGURED', 1);
83
define('LTI_TOOL_PROXY_STATE_PENDING', 2);
84
define('LTI_TOOL_PROXY_STATE_ACCEPTED', 3);
85
define('LTI_TOOL_PROXY_STATE_REJECTED', 4);
86
 
87
define('LTI_SETTING_NEVER', 0);
88
define('LTI_SETTING_ALWAYS', 1);
89
define('LTI_SETTING_DELEGATE', 2);
90
 
91
define('LTI_COURSEVISIBLE_NO', 0);
92
define('LTI_COURSEVISIBLE_PRECONFIGURED', 1);
93
define('LTI_COURSEVISIBLE_ACTIVITYCHOOSER', 2);
94
 
95
define('LTI_VERSION_1', 'LTI-1p0');
96
define('LTI_VERSION_2', 'LTI-2p0');
97
define('LTI_VERSION_1P3', '1.3.0');
98
define('LTI_RSA_KEY', 'RSA_KEY');
99
define('LTI_JWK_KEYSET', 'JWK_KEYSET');
100
 
101
define('LTI_DEFAULT_ORGID_SITEID', 'SITEID');
102
define('LTI_DEFAULT_ORGID_SITEHOST', 'SITEHOST');
103
 
104
define('LTI_ACCESS_TOKEN_LIFE', 3600);
105
 
106
// Standard prefix for JWT claims.
107
define('LTI_JWT_CLAIM_PREFIX', 'https://purl.imsglobal.org/spec/lti');
108
 
109
/**
110
 * Return the mapping for standard message types to JWT message_type claim.
111
 *
112
 * @return array
113
 */
114
function lti_get_jwt_message_type_mapping() {
115
    return array(
116
        'basic-lti-launch-request' => 'LtiResourceLinkRequest',
117
        'ContentItemSelectionRequest' => 'LtiDeepLinkingRequest',
118
        'LtiDeepLinkingResponse' => 'ContentItemSelection',
119
        'LtiSubmissionReviewRequest' => 'LtiSubmissionReviewRequest',
120
    );
121
}
122
 
123
/**
124
 * Return the mapping for standard message parameters to JWT claim.
125
 *
126
 * @return array
127
 */
128
function lti_get_jwt_claim_mapping() {
129
    $mapping = [];
130
    $services = lti_get_services();
131
    foreach ($services as $service) {
132
        $mapping = array_merge($mapping, $service->get_jwt_claim_mappings());
133
    }
134
    $mapping = array_merge($mapping, [
135
        'accept_copy_advice' => [
136
            'suffix' => 'dl',
137
            'group' => 'deep_linking_settings',
138
            'claim' => 'accept_copy_advice',
139
            'isarray' => false,
140
            'type' => 'boolean'
141
        ],
142
        'accept_media_types' => [
143
            'suffix' => 'dl',
144
            'group' => 'deep_linking_settings',
145
            'claim' => 'accept_media_types',
146
            'isarray' => true
147
        ],
148
        'accept_multiple' => [
149
            'suffix' => 'dl',
150
            'group' => 'deep_linking_settings',
151
            'claim' => 'accept_multiple',
152
            'isarray' => false,
153
            'type' => 'boolean'
154
        ],
155
        'accept_presentation_document_targets' => [
156
            'suffix' => 'dl',
157
            'group' => 'deep_linking_settings',
158
            'claim' => 'accept_presentation_document_targets',
159
            'isarray' => true
160
        ],
161
        'accept_types' => [
162
            'suffix' => 'dl',
163
            'group' => 'deep_linking_settings',
164
            'claim' => 'accept_types',
165
            'isarray' => true
166
        ],
167
        'accept_unsigned' => [
168
            'suffix' => 'dl',
169
            'group' => 'deep_linking_settings',
170
            'claim' => 'accept_unsigned',
171
            'isarray' => false,
172
            'type' => 'boolean'
173
        ],
174
        'auto_create' => [
175
            'suffix' => 'dl',
176
            'group' => 'deep_linking_settings',
177
            'claim' => 'auto_create',
178
            'isarray' => false,
179
            'type' => 'boolean'
180
        ],
181
        'can_confirm' => [
182
            'suffix' => 'dl',
183
            'group' => 'deep_linking_settings',
184
            'claim' => 'can_confirm',
185
            'isarray' => false,
186
            'type' => 'boolean'
187
        ],
188
        'content_item_return_url' => [
189
            'suffix' => 'dl',
190
            'group' => 'deep_linking_settings',
191
            'claim' => 'deep_link_return_url',
192
            'isarray' => false
193
        ],
194
        'content_items' => [
195
            'suffix' => 'dl',
196
            'group' => '',
197
            'claim' => 'content_items',
198
            'isarray' => true
199
        ],
200
        'data' => [
201
            'suffix' => 'dl',
202
            'group' => 'deep_linking_settings',
203
            'claim' => 'data',
204
            'isarray' => false
205
        ],
206
        'text' => [
207
            'suffix' => 'dl',
208
            'group' => 'deep_linking_settings',
209
            'claim' => 'text',
210
            'isarray' => false
211
        ],
212
        'title' => [
213
            'suffix' => 'dl',
214
            'group' => 'deep_linking_settings',
215
            'claim' => 'title',
216
            'isarray' => false
217
        ],
218
        'lti_msg' => [
219
            'suffix' => 'dl',
220
            'group' => '',
221
            'claim' => 'msg',
222
            'isarray' => false
223
        ],
224
        'lti_log' => [
225
            'suffix' => 'dl',
226
            'group' => '',
227
            'claim' => 'log',
228
            'isarray' => false
229
        ],
230
        'lti_errormsg' => [
231
            'suffix' => 'dl',
232
            'group' => '',
233
            'claim' => 'errormsg',
234
            'isarray' => false
235
        ],
236
        'lti_errorlog' => [
237
            'suffix' => 'dl',
238
            'group' => '',
239
            'claim' => 'errorlog',
240
            'isarray' => false
241
        ],
242
        'context_id' => [
243
            'suffix' => '',
244
            'group' => 'context',
245
            'claim' => 'id',
246
            'isarray' => false
247
        ],
248
        'context_label' => [
249
            'suffix' => '',
250
            'group' => 'context',
251
            'claim' => 'label',
252
            'isarray' => false
253
        ],
254
        'context_title' => [
255
            'suffix' => '',
256
            'group' => 'context',
257
            'claim' => 'title',
258
            'isarray' => false
259
        ],
260
        'context_type' => [
261
            'suffix' => '',
262
            'group' => 'context',
263
            'claim' => 'type',
264
            'isarray' => true
265
        ],
266
        'for_user_id' => [
267
            'suffix' => '',
268
            'group' => 'for_user',
269
            'claim' => 'user_id',
270
            'isarray' => false
271
        ],
272
        'lis_course_offering_sourcedid' => [
273
            'suffix' => '',
274
            'group' => 'lis',
275
            'claim' => 'course_offering_sourcedid',
276
            'isarray' => false
277
        ],
278
        'lis_course_section_sourcedid' => [
279
            'suffix' => '',
280
            'group' => 'lis',
281
            'claim' => 'course_section_sourcedid',
282
            'isarray' => false
283
        ],
284
        'launch_presentation_css_url' => [
285
            'suffix' => '',
286
            'group' => 'launch_presentation',
287
            'claim' => 'css_url',
288
            'isarray' => false
289
        ],
290
        'launch_presentation_document_target' => [
291
            'suffix' => '',
292
            'group' => 'launch_presentation',
293
            'claim' => 'document_target',
294
            'isarray' => false
295
        ],
296
        'launch_presentation_height' => [
297
            'suffix' => '',
298
            'group' => 'launch_presentation',
299
            'claim' => 'height',
300
            'isarray' => false
301
        ],
302
        'launch_presentation_locale' => [
303
            'suffix' => '',
304
            'group' => 'launch_presentation',
305
            'claim' => 'locale',
306
            'isarray' => false
307
        ],
308
        'launch_presentation_return_url' => [
309
            'suffix' => '',
310
            'group' => 'launch_presentation',
311
            'claim' => 'return_url',
312
            'isarray' => false
313
        ],
314
        'launch_presentation_width' => [
315
            'suffix' => '',
316
            'group' => 'launch_presentation',
317
            'claim' => 'width',
318
            'isarray' => false
319
        ],
320
        'lis_person_contact_email_primary' => [
321
            'suffix' => '',
322
            'group' => null,
323
            'claim' => 'email',
324
            'isarray' => false
325
        ],
326
        'lis_person_name_family' => [
327
            'suffix' => '',
328
            'group' => null,
329
            'claim' => 'family_name',
330
            'isarray' => false
331
        ],
332
        'lis_person_name_full' => [
333
            'suffix' => '',
334
            'group' => null,
335
            'claim' => 'name',
336
            'isarray' => false
337
        ],
338
        'lis_person_name_given' => [
339
            'suffix' => '',
340
            'group' => null,
341
            'claim' => 'given_name',
342
            'isarray' => false
343
        ],
344
        'lis_person_sourcedid' => [
345
            'suffix' => '',
346
            'group' => 'lis',
347
            'claim' => 'person_sourcedid',
348
            'isarray' => false
349
        ],
350
        'user_id' => [
351
            'suffix' => '',
352
            'group' => null,
353
            'claim' => 'sub',
354
            'isarray' => false
355
        ],
356
        'user_image' => [
357
            'suffix' => '',
358
            'group' => null,
359
            'claim' => 'picture',
360
            'isarray' => false
361
        ],
362
        'roles' => [
363
            'suffix' => '',
364
            'group' => '',
365
            'claim' => 'roles',
366
            'isarray' => true
367
        ],
368
        'role_scope_mentor' => [
369
            'suffix' => '',
370
            'group' => '',
371
            'claim' => 'role_scope_mentor',
372
            'isarray' => false
373
        ],
374
        'deployment_id' => [
375
            'suffix' => '',
376
            'group' => '',
377
            'claim' => 'deployment_id',
378
            'isarray' => false
379
        ],
380
        'lti_message_type' => [
381
            'suffix' => '',
382
            'group' => '',
383
            'claim' => 'message_type',
384
            'isarray' => false
385
        ],
386
        'lti_version' => [
387
            'suffix' => '',
388
            'group' => '',
389
            'claim' => 'version',
390
            'isarray' => false
391
        ],
392
        'resource_link_description' => [
393
            'suffix' => '',
394
            'group' => 'resource_link',
395
            'claim' => 'description',
396
            'isarray' => false
397
        ],
398
        'resource_link_id' => [
399
            'suffix' => '',
400
            'group' => 'resource_link',
401
            'claim' => 'id',
402
            'isarray' => false
403
        ],
404
        'resource_link_title' => [
405
            'suffix' => '',
406
            'group' => 'resource_link',
407
            'claim' => 'title',
408
            'isarray' => false
409
        ],
410
        'tool_consumer_info_product_family_code' => [
411
            'suffix' => '',
412
            'group' => 'tool_platform',
413
            'claim' => 'product_family_code',
414
            'isarray' => false
415
        ],
416
        'tool_consumer_info_version' => [
417
            'suffix' => '',
418
            'group' => 'tool_platform',
419
            'claim' => 'version',
420
            'isarray' => false
421
        ],
422
        'tool_consumer_instance_contact_email' => [
423
            'suffix' => '',
424
            'group' => 'tool_platform',
425
            'claim' => 'contact_email',
426
            'isarray' => false
427
        ],
428
        'tool_consumer_instance_description' => [
429
            'suffix' => '',
430
            'group' => 'tool_platform',
431
            'claim' => 'description',
432
            'isarray' => false
433
        ],
434
        'tool_consumer_instance_guid' => [
435
            'suffix' => '',
436
            'group' => 'tool_platform',
437
            'claim' => 'guid',
438
            'isarray' => false
439
        ],
440
        'tool_consumer_instance_name' => [
441
            'suffix' => '',
442
            'group' => 'tool_platform',
443
            'claim' => 'name',
444
            'isarray' => false
445
        ],
446
        'tool_consumer_instance_url' => [
447
            'suffix' => '',
448
            'group' => 'tool_platform',
449
            'claim' => 'url',
450
            'isarray' => false
451
        ]
452
    ]);
453
    return $mapping;
454
}
455
 
456
/**
457
 * Return the type of the instance, using domain matching if no explicit type is set.
458
 *
459
 * @param  object $instance the external tool activity settings
460
 * @return object|null
461
 * @since  Moodle 3.9
462
 */
463
function lti_get_instance_type(object $instance): ?object {
464
    if (empty($instance->typeid)) {
465
        if (!$tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course)) {
466
            $tool = lti_get_tool_by_url_match($instance->securetoolurl,  $instance->course);
467
        }
468
        return $tool;
469
    }
470
    return lti_get_type($instance->typeid);
471
}
472
 
473
/**
474
 * Return the launch data required for opening the external tool.
475
 *
476
 * @param  stdClass $instance the external tool activity settings
477
 * @param  string $nonce  the nonce value to use (applies to LTI 1.3 only)
478
 * @return array the endpoint URL and parameters (including the signature)
479
 * @since  Moodle 3.0
480
 */
481
function lti_get_launch_data($instance, $nonce = '', $messagetype = 'basic-lti-launch-request', $foruserid = 0) {
482
    global $PAGE, $USER;
483
    $messagetype = $messagetype ? $messagetype : 'basic-lti-launch-request';
484
    $tool = lti_get_instance_type($instance);
485
    if ($tool) {
486
        $typeid = $tool->id;
487
        $ltiversion = $tool->ltiversion;
488
    } else {
489
        $typeid = null;
490
        $ltiversion = LTI_VERSION_1;
491
    }
492
 
493
    if ($typeid) {
494
        $typeconfig = lti_get_type_config($typeid);
495
    } else {
496
        // There is no admin configuration for this tool. Use configuration in the lti instance record plus some defaults.
497
        $typeconfig = (array)$instance;
498
 
499
        $typeconfig['sendname'] = $instance->instructorchoicesendname;
500
        $typeconfig['sendemailaddr'] = $instance->instructorchoicesendemailaddr;
501
        $typeconfig['customparameters'] = $instance->instructorcustomparameters;
502
        $typeconfig['acceptgrades'] = $instance->instructorchoiceacceptgrades;
503
        $typeconfig['allowroster'] = $instance->instructorchoiceallowroster;
504
        $typeconfig['forcessl'] = '0';
505
    }
506
 
507
    if (isset($tool->toolproxyid)) {
508
        $toolproxy = lti_get_tool_proxy($tool->toolproxyid);
509
        $key = $toolproxy->guid;
510
        $secret = $toolproxy->secret;
511
    } else {
512
        $toolproxy = null;
513
        if (!empty($instance->resourcekey)) {
514
            $key = $instance->resourcekey;
515
        } else if ($ltiversion === LTI_VERSION_1P3) {
516
            $key = $tool->clientid;
517
        } else if (!empty($typeconfig['resourcekey'])) {
518
            $key = $typeconfig['resourcekey'];
519
        } else {
520
            $key = '';
521
        }
522
        if (!empty($instance->password)) {
523
            $secret = $instance->password;
524
        } else if (!empty($typeconfig['password'])) {
525
            $secret = $typeconfig['password'];
526
        } else {
527
            $secret = '';
528
        }
529
    }
530
 
531
    $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $typeconfig['toolurl'];
532
    $endpoint = trim($endpoint);
533
 
534
    // If the current request is using SSL and a secure tool URL is specified, use it.
535
    if (lti_request_is_using_ssl() && !empty($instance->securetoolurl)) {
536
        $endpoint = trim($instance->securetoolurl);
537
    }
538
 
539
    // If SSL is forced, use the secure tool url if specified. Otherwise, make sure https is on the normal launch URL.
540
    if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) {
541
        if (!empty($instance->securetoolurl)) {
542
            $endpoint = trim($instance->securetoolurl);
543
        }
544
 
545
        if ($endpoint !== '') {
546
            $endpoint = lti_ensure_url_is_https($endpoint);
547
        }
548
    } else if ($endpoint !== '' && !strstr($endpoint, '://')) {
549
        $endpoint = 'http://' . $endpoint;
550
    }
551
 
552
    $orgid = lti_get_organizationid($typeconfig);
553
 
554
    $course = $PAGE->course;
555
    $islti2 = isset($tool->toolproxyid);
556
    $allparams = lti_build_request($instance, $typeconfig, $course, $typeid, $islti2, $messagetype, $foruserid);
557
    if ($islti2) {
558
        $requestparams = lti_build_request_lti2($tool, $allparams);
559
    } else {
560
        $requestparams = $allparams;
561
    }
562
    $requestparams = array_merge($requestparams, lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype));
563
    $customstr = '';
564
    if (isset($typeconfig['customparameters'])) {
565
        $customstr = $typeconfig['customparameters'];
566
    }
567
    $services = lti_get_services();
568
    foreach ($services as $service) {
569
        [$endpoint, $customstr] = $service->override_endpoint($messagetype,
570
            $endpoint, $customstr, $instance->course, $instance);
571
    }
572
    $requestparams = array_merge($requestparams, lti_build_custom_parameters($toolproxy, $tool, $instance, $allparams, $customstr,
573
        $instance->instructorcustomparameters, $islti2));
574
 
575
    $launchcontainer = lti_get_launch_container($instance, $typeconfig);
576
    $returnurlparams = array('course' => $course->id,
577
        'launch_container' => $launchcontainer,
578
        'instanceid' => $instance->id,
579
        'sesskey' => sesskey());
580
 
581
    // Add the return URL. We send the launch container along to help us avoid frames-within-frames when the user returns.
582
    $url = new \moodle_url('/mod/lti/return.php', $returnurlparams);
583
    $returnurl = $url->out(false);
584
 
585
    if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) {
586
        $returnurl = lti_ensure_url_is_https($returnurl);
587
    }
588
 
589
    $target = '';
590
    switch($launchcontainer) {
591
        case LTI_LAUNCH_CONTAINER_EMBED:
592
        case LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS:
593
            $target = 'iframe';
594
            break;
595
        case LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW:
596
            $target = 'frame';
597
            break;
598
        case LTI_LAUNCH_CONTAINER_WINDOW:
599
            $target = 'window';
600
            break;
601
    }
602
    if (!empty($target)) {
603
        $requestparams['launch_presentation_document_target'] = $target;
604
    }
605
 
606
    $requestparams['launch_presentation_return_url'] = $returnurl;
607
 
608
    // Add the parameters configured by the LTI services.
609
    if ($typeid && !$islti2) {
610
        $services = lti_get_services();
611
        foreach ($services as $service) {
612
            $serviceparameters = $service->get_launch_parameters('basic-lti-launch-request',
613
                    $course->id, $USER->id , $typeid, $instance->id);
614
            foreach ($serviceparameters as $paramkey => $paramvalue) {
615
                $requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue,
616
                    $islti2);
617
            }
618
        }
619
    }
620
 
621
    // Allow request params to be updated by sub-plugins.
622
    $plugins = core_component::get_plugin_list('ltisource');
623
    foreach (array_keys($plugins) as $plugin) {
624
        $pluginparams = component_callback('ltisource_'.$plugin, 'before_launch',
625
            array($instance, $endpoint, $requestparams), array());
626
 
627
        if (!empty($pluginparams) && is_array($pluginparams)) {
628
            $requestparams = array_merge($requestparams, $pluginparams);
629
        }
630
    }
631
 
632
    if ((!empty($key) && !empty($secret)) || ($ltiversion === LTI_VERSION_1P3)) {
633
        if ($ltiversion !== LTI_VERSION_1P3) {
634
            $parms = lti_sign_parameters($requestparams, $endpoint, 'POST', $key, $secret);
635
        } else {
636
            $parms = lti_sign_jwt($requestparams, $endpoint, $key, $typeid, $nonce);
637
        }
638
 
639
        $endpointurl = new \moodle_url($endpoint);
640
        $endpointparams = $endpointurl->params();
641
 
642
        // Strip querystring params in endpoint url from $parms to avoid duplication.
643
        if (!empty($endpointparams) && !empty($parms)) {
644
            foreach (array_keys($endpointparams) as $paramname) {
645
                if (isset($parms[$paramname])) {
646
                    unset($parms[$paramname]);
647
                }
648
            }
649
        }
650
 
651
    } else {
652
        // If no key and secret, do the launch unsigned.
653
        $returnurlparams['unsigned'] = '1';
654
        $parms = $requestparams;
655
    }
656
 
657
    return array($endpoint, $parms);
658
}
659
 
660
/**
661
 * Launch an external tool activity.
662
 *
663
 * @param stdClass $instance the external tool activity settings
664
 * @param int $foruserid for user param, optional
665
 * @return string The HTML code containing the javascript code for the launch
666
 */
667
function lti_launch_tool($instance, $foruserid=0) {
668
 
669
    list($endpoint, $parms) = lti_get_launch_data($instance, '', '', $foruserid);
670
    $debuglaunch = ( $instance->debuglaunch == 1 );
671
 
672
    $content = lti_post_launch_html($parms, $endpoint, $debuglaunch);
673
 
674
    echo $content;
675
}
676
 
677
/**
678
 * Prepares an LTI registration request message
679
 *
680
 * @param object $toolproxy  Tool Proxy instance object
681
 */
682
function lti_register($toolproxy) {
683
    $endpoint = $toolproxy->regurl;
684
 
685
    // Change the status to pending.
686
    $toolproxy->state = LTI_TOOL_PROXY_STATE_PENDING;
687
    lti_update_tool_proxy($toolproxy);
688
 
689
    $requestparams = lti_build_registration_request($toolproxy);
690
 
691
    $content = lti_post_launch_html($requestparams, $endpoint, false);
692
 
693
    echo $content;
694
}
695
 
696
 
697
/**
698
 * Gets the parameters for the regirstration request
699
 *
700
 * @param object $toolproxy Tool Proxy instance object
701
 * @return array Registration request parameters
702
 */
703
function lti_build_registration_request($toolproxy) {
704
    $key = $toolproxy->guid;
705
    $secret = $toolproxy->secret;
706
 
707
    $requestparams = array();
708
    $requestparams['lti_message_type'] = 'ToolProxyRegistrationRequest';
709
    $requestparams['lti_version'] = 'LTI-2p0';
710
    $requestparams['reg_key'] = $key;
711
    $requestparams['reg_password'] = $secret;
712
    $requestparams['reg_url'] = $toolproxy->regurl;
713
 
714
    // Add the profile URL.
715
    $profileservice = lti_get_service_by_name('profile');
716
    $profileservice->set_tool_proxy($toolproxy);
717
    $requestparams['tc_profile_url'] = $profileservice->parse_value('$ToolConsumerProfile.url');
718
 
719
    // Add the return URL.
720
    $returnurlparams = array('id' => $toolproxy->id, 'sesskey' => sesskey());
721
    $url = new \moodle_url('/mod/lti/externalregistrationreturn.php', $returnurlparams);
722
    $returnurl = $url->out(false);
723
 
724
    $requestparams['launch_presentation_return_url'] = $returnurl;
725
 
726
    return $requestparams;
727
}
728
 
729
 
730
/** get Organization ID using default if no value provided
731
 * @param object $typeconfig
732
 * @return string
733
 */
734
function lti_get_organizationid($typeconfig) {
735
    global $CFG;
736
    // Default the organizationid if not specified.
737
    if (empty($typeconfig['organizationid'])) {
738
        if (($typeconfig['organizationid_default'] ?? LTI_DEFAULT_ORGID_SITEHOST) == LTI_DEFAULT_ORGID_SITEHOST) {
739
            $urlparts = parse_url($CFG->wwwroot);
740
            return $urlparts['host'];
741
        } else {
742
            return md5(get_site_identifier());
743
        }
744
    }
745
    return $typeconfig['organizationid'];
746
}
747
 
748
/**
749
 * Build source ID
750
 *
751
 * @param int $instanceid
752
 * @param int $userid
753
 * @param string $servicesalt
754
 * @param null|int $typeid
755
 * @param null|int $launchid
756
 * @return stdClass
757
 */
758
function lti_build_sourcedid($instanceid, $userid, $servicesalt, $typeid = null, $launchid = null) {
759
    $data = new \stdClass();
760
 
761
    $data->instanceid = $instanceid;
762
    $data->userid = $userid;
763
    $data->typeid = $typeid;
764
    if (!empty($launchid)) {
765
        $data->launchid = $launchid;
766
    } else {
767
        $data->launchid = mt_rand();
768
    }
769
 
770
    $json = json_encode($data);
771
 
772
    $hash = hash('sha256', $json . $servicesalt, false);
773
 
774
    $container = new \stdClass();
775
    $container->data = $data;
776
    $container->hash = $hash;
777
 
778
    return $container;
779
}
780
 
781
/**
782
 * This function builds the request that must be sent to the tool producer
783
 *
784
 * @param object    $instance       Basic LTI instance object
785
 * @param array     $typeconfig     Basic LTI tool configuration
786
 * @param object    $course         Course object
787
 * @param int|null  $typeid         Basic LTI tool ID
788
 * @param boolean   $islti2         True if an LTI 2 tool is being launched
789
 * @param string    $messagetype    LTI Message Type for this launch
790
 * @param int       $foruserid      User targeted by this launch
791
 *
792
 * @return array                    Request details
793
 */
794
function lti_build_request($instance, $typeconfig, $course, $typeid = null, $islti2 = false,
795
    $messagetype = 'basic-lti-launch-request', $foruserid = 0) {
796
    global $USER, $CFG;
797
 
798
    if (empty($instance->cmid)) {
799
        $instance->cmid = 0;
800
    }
801
 
802
    $role = lti_get_ims_role($USER, $instance->cmid, $instance->course, $islti2);
803
 
804
    $requestparams = array(
805
        'user_id' => $USER->id,
806
        'lis_person_sourcedid' => $USER->idnumber,
807
        'roles' => $role,
808
        'context_id' => $course->id,
809
        'context_label' => trim(html_to_text($course->shortname, 0)),
810
        'context_title' => trim(html_to_text($course->fullname, 0)),
811
    );
812
    if ($foruserid) {
813
        $requestparams['for_user_id'] = $foruserid;
814
    }
815
    if ($messagetype) {
816
        $requestparams['lti_message_type'] = $messagetype;
817
    }
818
    if (!empty($instance->name)) {
819
        $requestparams['resource_link_title'] = trim(html_to_text($instance->name, 0));
820
    }
821
    if (!empty($instance->cmid)) {
822
        $intro = format_module_intro('lti', $instance, $instance->cmid);
823
        $intro = trim(html_to_text($intro, 0, false));
824
 
825
        // This may look weird, but this is required for new lines
826
        // so we generate the same OAuth signature as the tool provider.
827
        $intro = str_replace("\n", "\r\n", $intro);
828
        $requestparams['resource_link_description'] = $intro;
829
    }
830
    if (!empty($instance->id)) {
831
        $requestparams['resource_link_id'] = $instance->id;
832
    }
833
    if (!empty($instance->resource_link_id)) {
834
        $requestparams['resource_link_id'] = $instance->resource_link_id;
835
    }
836
    if ($course->format == 'site') {
837
        $requestparams['context_type'] = 'Group';
838
    } else {
839
        $requestparams['context_type'] = 'CourseSection';
840
        $requestparams['lis_course_section_sourcedid'] = $course->idnumber;
841
    }
842
 
843
    if (!empty($instance->id) && !empty($instance->servicesalt) && ($islti2 ||
844
            $typeconfig['acceptgrades'] == LTI_SETTING_ALWAYS ||
845
            ($typeconfig['acceptgrades'] == LTI_SETTING_DELEGATE && $instance->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS))
846
    ) {
847
        $placementsecret = $instance->servicesalt;
848
        $sourcedid = json_encode(lti_build_sourcedid($instance->id, $USER->id, $placementsecret, $typeid));
849
        $requestparams['lis_result_sourcedid'] = $sourcedid;
850
 
851
        // Add outcome service URL.
852
        $serviceurl = new \moodle_url('/mod/lti/service.php');
853
        $serviceurl = $serviceurl->out();
854
 
855
        $forcessl = false;
856
        if (!empty($CFG->mod_lti_forcessl)) {
857
            $forcessl = true;
858
        }
859
 
860
        if ((isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) or $forcessl) {
861
            $serviceurl = lti_ensure_url_is_https($serviceurl);
862
        }
863
 
864
        $requestparams['lis_outcome_service_url'] = $serviceurl;
865
    }
866
 
867
    // Send user's name and email data if appropriate.
868
    if ($islti2 || $typeconfig['sendname'] == LTI_SETTING_ALWAYS ||
869
        ($typeconfig['sendname'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendname)
870
            && $instance->instructorchoicesendname == LTI_SETTING_ALWAYS)
871
    ) {
872
        $requestparams['lis_person_name_given'] = $USER->firstname;
873
        $requestparams['lis_person_name_family'] = $USER->lastname;
874
        $requestparams['lis_person_name_full'] = fullname($USER);
875
        $requestparams['ext_user_username'] = $USER->username;
876
    }
877
 
878
    if ($islti2 || $typeconfig['sendemailaddr'] == LTI_SETTING_ALWAYS ||
879
        ($typeconfig['sendemailaddr'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendemailaddr)
880
            && $instance->instructorchoicesendemailaddr == LTI_SETTING_ALWAYS)
881
    ) {
882
        $requestparams['lis_person_contact_email_primary'] = $USER->email;
883
    }
884
 
885
    return $requestparams;
886
}
887
 
888
/**
889
 * This function builds the request that must be sent to an LTI 2 tool provider
890
 *
891
 * @param object    $tool           Basic LTI tool object
892
 * @param array     $params         Custom launch parameters
893
 *
894
 * @return array                    Request details
895
 */
896
function lti_build_request_lti2($tool, $params) {
897
 
898
    $requestparams = array();
899
 
900
    $capabilities = lti_get_capabilities();
901
    $enabledcapabilities = explode("\n", $tool->enabledcapability);
902
    foreach ($enabledcapabilities as $capability) {
903
        if (array_key_exists($capability, $capabilities)) {
904
            $val = $capabilities[$capability];
905
            if ($val && (substr($val, 0, 1) != '$')) {
906
                if (isset($params[$val])) {
907
                    $requestparams[$capabilities[$capability]] = $params[$capabilities[$capability]];
908
                }
909
            }
910
        }
911
    }
912
 
913
    return $requestparams;
914
 
915
}
916
 
917
/**
918
 * This function builds the standard parameters for an LTI 1 or 2 request that must be sent to the tool producer
919
 *
920
 * @param stdClass  $instance       Basic LTI instance object
921
 * @param string    $orgid          Organisation ID
922
 * @param boolean   $islti2         True if an LTI 2 tool is being launched
923
 * @param string    $messagetype    The request message type. Defaults to basic-lti-launch-request if empty.
924
 *
925
 * @return array                    Request details
926
 * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more.
927
 * @see lti_build_standard_message()
928
 */
929
function lti_build_standard_request($instance, $orgid, $islti2, $messagetype = 'basic-lti-launch-request') {
930
    if (!$islti2) {
931
        $ltiversion = LTI_VERSION_1;
932
    } else {
933
        $ltiversion = LTI_VERSION_2;
934
    }
935
    return lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype);
936
}
937
 
938
/**
939
 * This function builds the standard parameters for an LTI message that must be sent to the tool producer
940
 *
941
 * @param stdClass  $instance       Basic LTI instance object
942
 * @param string    $orgid          Organisation ID
943
 * @param boolean   $ltiversion     LTI version to be used for tool messages
944
 * @param string    $messagetype    The request message type. Defaults to basic-lti-launch-request if empty.
945
 *
946
 * @return array                    Message parameters
947
 */
948
function lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype = 'basic-lti-launch-request') {
949
    global $CFG;
950
 
951
    $requestparams = array();
952
 
953
    if ($instance) {
954
        $requestparams['resource_link_id'] = $instance->id;
955
        if (property_exists($instance, 'resource_link_id') and !empty($instance->resource_link_id)) {
956
            $requestparams['resource_link_id'] = $instance->resource_link_id;
957
        }
958
    }
959
 
960
    $requestparams['launch_presentation_locale'] = current_language();
961
 
962
    // Make sure we let the tool know what LMS they are being called from.
963
    $requestparams['ext_lms'] = 'moodle-2';
964
    $requestparams['tool_consumer_info_product_family_code'] = 'moodle';
965
    $requestparams['tool_consumer_info_version'] = strval($CFG->version);
966
 
967
    // Add oauth_callback to be compliant with the 1.0A spec.
968
    $requestparams['oauth_callback'] = 'about:blank';
969
 
970
    $requestparams['lti_version'] = $ltiversion;
971
    $requestparams['lti_message_type'] = $messagetype;
972
 
973
    if ($orgid) {
974
        $requestparams["tool_consumer_instance_guid"] = $orgid;
975
    }
976
    if (!empty($CFG->mod_lti_institution_name)) {
977
        $requestparams['tool_consumer_instance_name'] = trim(html_to_text($CFG->mod_lti_institution_name, 0));
978
    } else {
979
        $requestparams['tool_consumer_instance_name'] = get_site()->shortname;
980
    }
981
    $requestparams['tool_consumer_instance_description'] = trim(html_to_text(get_site()->fullname, 0));
982
 
983
    return $requestparams;
984
}
985
 
986
/**
987
 * This function builds the custom parameters
988
 *
989
 * @param object    $toolproxy      Tool proxy instance object
990
 * @param object    $tool           Tool instance object
991
 * @param object    $instance       Tool placement instance object
992
 * @param array     $params         LTI launch parameters
993
 * @param string    $customstr      Custom parameters defined for tool
994
 * @param string    $instructorcustomstr      Custom parameters defined for this placement
995
 * @param boolean   $islti2         True if an LTI 2 tool is being launched
996
 *
997
 * @return array                    Custom parameters
998
 */
999
function lti_build_custom_parameters($toolproxy, $tool, $instance, $params, $customstr, $instructorcustomstr, $islti2) {
1000
 
1001
    // Concatenate the custom parameters from the administrator and the instructor
1002
    // Instructor parameters are only taken into consideration if the administrator
1003
    // has given permission.
1004
    $custom = array();
1005
    if ($customstr) {
1006
        $custom = lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2);
1007
    }
1008
    if ($instructorcustomstr) {
1009
        $custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params,
1010
            $instructorcustomstr, $islti2), $custom);
1011
    }
1012
    if ($islti2) {
1013
        $custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params,
1014
            $tool->parameter, true), $custom);
1015
        $settings = lti_get_tool_settings($tool->toolproxyid);
1016
        $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));
1017
        if (!empty($instance->course)) {
1018
            $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course);
1019
            $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));
1020
            if (!empty($instance->id)) {
1021
                $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course, $instance->id);
1022
                $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));
1023
            }
1024
        }
1025
    }
1026
 
1027
    return $custom;
1028
}
1029
 
1030
/**
1031
 * Builds a standard LTI Content-Item selection request.
1032
 *
1033
 * @param int $id The tool type ID.
1034
 * @param stdClass $course The course object.
1035
 * @param moodle_url $returnurl The return URL in the tool consumer (TC) that the tool provider (TP)
1036
 *                              will use to return the Content-Item message.
1037
 * @param string $title The tool's title, if available.
1038
 * @param string $text The text to display to represent the content item. This value may be a long description of the content item.
1039
 * @param array $mediatypes Array of MIME types types supported by the TC. If empty, the TC will support ltilink by default.
1040
 * @param array $presentationtargets Array of ways in which the selected content item(s) can be requested to be opened
1041
 *                                   (via the presentationDocumentTarget element for a returned content item).
1042
 *                                   If empty, "frame", "iframe", and "window" will be supported by default.
1043
 * @param bool $autocreate Indicates whether any content items returned by the TP would be automatically persisted without
1044
 * @param bool $multiple Indicates whether the user should be permitted to select more than one item. False by default.
1045
 *                         any option for the user to cancel the operation. False by default.
1046
 * @param bool $unsigned Indicates whether the TC is willing to accept an unsigned return message, or not.
1047
 *                       A signed message should always be required when the content item is being created automatically in the
1048
 *                       TC without further interaction from the user. False by default.
1049
 * @param bool $canconfirm Flag for can_confirm parameter. False by default.
1050
 * @param bool $copyadvice Indicates whether the TC is able and willing to make a local copy of a content item. False by default.
1051
 * @param string $nonce
1052
 * @return stdClass The object containing the signed request parameters and the URL to the TP's Content-Item selection interface.
1053
 * @throws moodle_exception When the LTI tool type does not exist.`
1054
 * @throws coding_exception For invalid media type and presentation target parameters.
1055
 */
1056
function lti_build_content_item_selection_request($id, $course, moodle_url $returnurl, $title = '', $text = '', $mediatypes = [],
1057
                                                  $presentationtargets = [], $autocreate = false, $multiple = true,
1058
                                                  $unsigned = false, $canconfirm = false, $copyadvice = false, $nonce = '') {
1059
    global $USER;
1060
 
1061
    $tool = lti_get_type($id);
1062
    // Validate parameters.
1063
    if (!$tool) {
1064
        throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1065
    }
1066
    if (!is_array($mediatypes)) {
1067
        throw new coding_exception('The list of accepted media types should be in an array');
1068
    }
1069
    if (!is_array($presentationtargets)) {
1070
        throw new coding_exception('The list of accepted presentation targets should be in an array');
1071
    }
1072
 
1073
    // Check title. If empty, use the tool's name.
1074
    if (empty($title)) {
1075
        $title = $tool->name;
1076
    }
1077
 
1078
    $typeconfig = lti_get_type_config($id);
1079
    $key = '';
1080
    $secret = '';
1081
    $islti2 = false;
1082
    $islti13 = false;
1083
    if (isset($tool->toolproxyid)) {
1084
        $islti2 = true;
1085
        $toolproxy = lti_get_tool_proxy($tool->toolproxyid);
1086
        $key = $toolproxy->guid;
1087
        $secret = $toolproxy->secret;
1088
    } else {
1089
        $islti13 = $tool->ltiversion === LTI_VERSION_1P3;
1090
        $toolproxy = null;
1091
        if ($islti13 && !empty($tool->clientid)) {
1092
            $key = $tool->clientid;
1093
        } else if (!$islti13 && !empty($typeconfig['resourcekey'])) {
1094
            $key = $typeconfig['resourcekey'];
1095
        }
1096
        if (!empty($typeconfig['password'])) {
1097
            $secret = $typeconfig['password'];
1098
        }
1099
    }
1100
    $tool->enabledcapability = '';
1101
    if (!empty($typeconfig['enabledcapability_ContentItemSelectionRequest'])) {
1102
        $tool->enabledcapability = $typeconfig['enabledcapability_ContentItemSelectionRequest'];
1103
    }
1104
 
1105
    $tool->parameter = '';
1106
    if (!empty($typeconfig['parameter_ContentItemSelectionRequest'])) {
1107
        $tool->parameter = $typeconfig['parameter_ContentItemSelectionRequest'];
1108
    }
1109
 
1110
    // Set the tool URL.
1111
    if (!empty($typeconfig['toolurl_ContentItemSelectionRequest'])) {
1112
        $toolurl = new moodle_url($typeconfig['toolurl_ContentItemSelectionRequest']);
1113
    } else {
1114
        $toolurl = new moodle_url($typeconfig['toolurl']);
1115
    }
1116
 
1117
    // Check if SSL is forced.
1118
    if (!empty($typeconfig['forcessl'])) {
1119
        // Make sure the tool URL is set to https.
1120
        if (strtolower($toolurl->get_scheme()) === 'http') {
1121
            $toolurl->set_scheme('https');
1122
        }
1123
        // Make sure the return URL is set to https.
1124
        if (strtolower($returnurl->get_scheme()) === 'http') {
1125
            $returnurl->set_scheme('https');
1126
        }
1127
    }
1128
    $toolurlout = $toolurl->out(false);
1129
 
1130
    // Get base request parameters.
1131
    $instance = new stdClass();
1132
    $instance->course = $course->id;
1133
    $requestparams = lti_build_request($instance, $typeconfig, $course, $id, $islti2);
1134
 
1135
    // Get LTI2-specific request parameters and merge to the request parameters if applicable.
1136
    if ($islti2) {
1137
        $lti2params = lti_build_request_lti2($tool, $requestparams);
1138
        $requestparams = array_merge($requestparams, $lti2params);
1139
    }
1140
 
1141
    // Get standard request parameters and merge to the request parameters.
1142
    $orgid = lti_get_organizationid($typeconfig);
1143
    $standardparams = lti_build_standard_message(null, $orgid, $tool->ltiversion, 'ContentItemSelectionRequest');
1144
    $requestparams = array_merge($requestparams, $standardparams);
1145
 
1146
    // Get custom request parameters and merge to the request parameters.
1147
    $customstr = '';
1148
    if (!empty($typeconfig['customparameters'])) {
1149
        $customstr = $typeconfig['customparameters'];
1150
    }
1151
    $customparams = lti_build_custom_parameters($toolproxy, $tool, $instance, $requestparams, $customstr, '', $islti2);
1152
    $requestparams = array_merge($requestparams, $customparams);
1153
 
1154
    // Add the parameters configured by the LTI services.
1155
    if ($id && !$islti2) {
1156
        $services = lti_get_services();
1157
        foreach ($services as $service) {
1158
            $serviceparameters = $service->get_launch_parameters('ContentItemSelectionRequest',
1159
                $course->id, $USER->id , $id);
1160
            foreach ($serviceparameters as $paramkey => $paramvalue) {
1161
                $requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue,
1162
                    $islti2);
1163
            }
1164
        }
1165
    }
1166
 
1167
    // Allow request params to be updated by sub-plugins.
1168
    $plugins = core_component::get_plugin_list('ltisource');
1169
    foreach (array_keys($plugins) as $plugin) {
1170
        $pluginparams = component_callback('ltisource_' . $plugin, 'before_launch', [$instance, $toolurlout, $requestparams], []);
1171
 
1172
        if (!empty($pluginparams) && is_array($pluginparams)) {
1173
            $requestparams = array_merge($requestparams, $pluginparams);
1174
        }
1175
    }
1176
 
1177
    if (!$islti13) {
1178
        // Media types. Set to ltilink by default if empty.
1179
        if (empty($mediatypes)) {
1180
            $mediatypes = [
1181
                'application/vnd.ims.lti.v1.ltilink',
1182
            ];
1183
        }
1184
        $requestparams['accept_media_types'] = implode(',', $mediatypes);
1185
    } else {
1186
        // Only LTI links are currently supported.
1187
        $requestparams['accept_types'] = 'ltiResourceLink';
1188
    }
1189
 
1190
    // Presentation targets. Supports frame, iframe, window by default if empty.
1191
    if (empty($presentationtargets)) {
1192
        $presentationtargets = [
1193
            'frame',
1194
            'iframe',
1195
            'window',
1196
        ];
1197
    }
1198
    $requestparams['accept_presentation_document_targets'] = implode(',', $presentationtargets);
1199
 
1200
    // Other request parameters.
1201
    $requestparams['accept_copy_advice'] = $copyadvice === true ? 'true' : 'false';
1202
    $requestparams['accept_multiple'] = $multiple === true ? 'true' : 'false';
1203
    $requestparams['accept_unsigned'] = $unsigned === true ? 'true' : 'false';
1204
    $requestparams['auto_create'] = $autocreate === true ? 'true' : 'false';
1205
    $requestparams['can_confirm'] = $canconfirm === true ? 'true' : 'false';
1206
    $requestparams['content_item_return_url'] = $returnurl->out(false);
1207
    $requestparams['title'] = $title;
1208
    $requestparams['text'] = $text;
1209
    if (!$islti13) {
1210
        $signedparams = lti_sign_parameters($requestparams, $toolurlout, 'POST', $key, $secret);
1211
    } else {
1212
        $signedparams = lti_sign_jwt($requestparams, $toolurlout, $key, $id, $nonce);
1213
    }
1214
    $toolurlparams = $toolurl->params();
1215
 
1216
    // Strip querystring params in endpoint url from $signedparams to avoid duplication.
1217
    if (!empty($toolurlparams) && !empty($signedparams)) {
1218
        foreach (array_keys($toolurlparams) as $paramname) {
1219
            if (isset($signedparams[$paramname])) {
1220
                unset($signedparams[$paramname]);
1221
            }
1222
        }
1223
    }
1224
 
1225
    // Check for params that should not be passed. Unset if they are set.
1226
    $unwantedparams = [
1227
        'resource_link_id',
1228
        'resource_link_title',
1229
        'resource_link_description',
1230
        'launch_presentation_return_url',
1231
        'lis_result_sourcedid',
1232
    ];
1233
    foreach ($unwantedparams as $param) {
1234
        if (isset($signedparams[$param])) {
1235
            unset($signedparams[$param]);
1236
        }
1237
    }
1238
 
1239
    // Prepare result object.
1240
    $result = new stdClass();
1241
    $result->params = $signedparams;
1242
    $result->url = $toolurlout;
1243
 
1244
    return $result;
1245
}
1246
 
1247
/**
1248
 * Verifies the OAuth signature of an incoming message.
1249
 *
1250
 * @param int $typeid The tool type ID.
1251
 * @param string $consumerkey The consumer key.
1252
 * @return stdClass Tool type
1253
 * @throws moodle_exception
1254
 * @throws lti\OAuthException
1255
 */
1256
function lti_verify_oauth_signature($typeid, $consumerkey) {
1257
    $tool = lti_get_type($typeid);
1258
    // Validate parameters.
1259
    if (!$tool) {
1260
        throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1261
    }
1262
    $typeconfig = lti_get_type_config($typeid);
1263
 
1264
    if (isset($tool->toolproxyid)) {
1265
        $toolproxy = lti_get_tool_proxy($tool->toolproxyid);
1266
        $key = $toolproxy->guid;
1267
        $secret = $toolproxy->secret;
1268
    } else {
1269
        $toolproxy = null;
1270
        if (!empty($typeconfig['resourcekey'])) {
1271
            $key = $typeconfig['resourcekey'];
1272
        } else {
1273
            $key = '';
1274
        }
1275
        if (!empty($typeconfig['password'])) {
1276
            $secret = $typeconfig['password'];
1277
        } else {
1278
            $secret = '';
1279
        }
1280
    }
1281
 
1282
    if ($consumerkey !== $key) {
1283
        throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti');
1284
    }
1285
 
1286
    $store = new lti\TrivialOAuthDataStore();
1287
    $store->add_consumer($key, $secret);
1288
    $server = new lti\OAuthServer($store);
1289
    $method = new lti\OAuthSignatureMethod_HMAC_SHA1();
1290
    $server->add_signature_method($method);
1291
    $request = lti\OAuthRequest::from_request();
1292
    try {
1293
        $server->verify_request($request);
1294
    } catch (lti\OAuthException $e) {
1295
        throw new lti\OAuthException("OAuth signature failed: " . $e->getMessage());
1296
    }
1297
 
1298
    return $tool;
1299
}
1300
 
1301
/**
1302
 * Verifies the JWT signature using a JWK keyset.
1303
 *
1304
 * @param string $jwtparam JWT parameter value.
1305
 * @param string $keyseturl The tool keyseturl.
1306
 * @param string $clientid The tool client id.
1307
 *
1308
 * @return object The JWT's payload as a PHP object
1309
 * @throws moodle_exception
1310
 * @throws UnexpectedValueException     Provided JWT was invalid
1311
 * @throws SignatureInvalidException    Provided JWT was invalid because the signature verification failed
1312
 * @throws BeforeValidException         Provided JWT is trying to be used before it's eligible as defined by 'nbf'
1313
 * @throws BeforeValidException         Provided JWT is trying to be used before it's been created as defined by 'iat'
1314
 * @throws ExpiredException             Provided JWT has since expired, as defined by the 'exp' claim
1315
 */
1316
function lti_verify_with_keyset($jwtparam, $keyseturl, $clientid) {
1317
    // Attempts to retrieve cached keyset.
1318
    $cache = cache::make('mod_lti', 'keyset');
1319
    $keyset = $cache->get($clientid);
1320
 
1321
    try {
1322
        if (empty($keyset)) {
1323
            throw new moodle_exception('errornocachedkeysetfound', 'mod_lti');
1324
        }
1325
        $keysetarr = json_decode($keyset, true);
1326
        $keys = JWK::parseKeySet($keysetarr);
1327
        $jwt = JWT::decode($jwtparam, $keys);
1328
    } catch (Exception $e) {
1329
        // Something went wrong, so attempt to update cached keyset and then try again.
1330
        $keyset = download_file_content($keyseturl);
1331
        $keysetarr = json_decode($keyset, true);
1332
 
1333
        // Fix for firebase/php-jwt's dependency on the optional 'alg' property in the JWK.
1441 ariadna 1334
        // The fix_jwks_alg() call only fixes a single, matched key and will leave others present (which may be missing alg too),
1335
        // Remaining keys missing alg are excluded since they cannot be used for decoding anyway (no match to JWT kid).
1 efrain 1336
        $keysetarr = jwks_helper::fix_jwks_alg($keysetarr, $jwtparam);
1441 ariadna 1337
        $keysetarr['keys'] = array_filter($keysetarr['keys'], fn($key) => isset($key['alg']));
1 efrain 1338
 
1339
        // JWK::parseKeySet uses RS256 algorithm by default.
1340
        $keys = JWK::parseKeySet($keysetarr);
1341
        $jwt = JWT::decode($jwtparam, $keys);
1342
        // If sucessful, updates the cached keyset.
1343
        $cache->set($clientid, $keyset);
1344
    }
1345
    return $jwt;
1346
}
1347
 
1348
/**
1349
 * Verifies the JWT signature of an incoming message.
1350
 *
1351
 * @param int $typeid The tool type ID.
1352
 * @param string $consumerkey The consumer key.
1353
 * @param string $jwtparam JWT parameter value
1354
 *
1355
 * @return stdClass Tool type
1356
 * @throws moodle_exception
1357
 * @throws UnexpectedValueException     Provided JWT was invalid
1358
 * @throws SignatureInvalidException    Provided JWT was invalid because the signature verification failed
1359
 * @throws BeforeValidException         Provided JWT is trying to be used before it's eligible as defined by 'nbf'
1360
 * @throws BeforeValidException         Provided JWT is trying to be used before it's been created as defined by 'iat'
1361
 * @throws ExpiredException             Provided JWT has since expired, as defined by the 'exp' claim
1362
 */
1363
function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) {
1364
    $tool = lti_get_type($typeid);
1365
 
1366
    // Validate parameters.
1367
    if (!$tool) {
1368
        throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1369
    }
1370
    if (isset($tool->toolproxyid)) {
1371
        throw new moodle_exception('JWT security not supported with LTI 2');
1372
    }
1373
 
1374
    $typeconfig = lti_get_type_config($typeid);
1375
 
1376
    $key = $tool->clientid ?? '';
1377
 
1378
    if ($consumerkey !== $key) {
1379
        throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti');
1380
    }
1381
 
1382
    if (empty($typeconfig['keytype']) || $typeconfig['keytype'] === LTI_RSA_KEY) {
1383
        $publickey = $typeconfig['publickey'] ?? '';
1384
        if (empty($publickey)) {
1385
            throw new moodle_exception('No public key configured');
1386
        }
1387
        // Attemps to verify jwt with RSA key.
1388
        JWT::decode($jwtparam, new Key($publickey, 'RS256'));
1389
    } else if ($typeconfig['keytype'] === LTI_JWK_KEYSET) {
1390
        $keyseturl = $typeconfig['publickeyset'] ?? '';
1391
        if (empty($keyseturl)) {
1392
            throw new moodle_exception('No public keyset configured');
1393
        }
1394
        // Attempts to verify jwt with jwk keyset.
1395
        lti_verify_with_keyset($jwtparam, $keyseturl, $tool->clientid);
1396
    } else {
1397
        throw new moodle_exception('Invalid public key type');
1398
    }
1399
 
1400
    return $tool;
1401
}
1402
 
1403
/**
1404
 * Converts an array of custom parameters to a new line separated string.
1405
 *
1406
 * @param object $params list of params to concatenate
1407
 *
1408
 * @return string
1409
 */
1410
function params_to_string(object $params) {
1411
    $customparameters = [];
1412
    foreach ($params as $key => $value) {
1413
        $customparameters[] = "{$key}={$value}";
1414
    }
1415
    return implode("\n", $customparameters);
1416
}
1417
 
1418
/**
1419
 * Converts LTI 1.1 Content Item for LTI Link to Form data.
1420
 *
1421
 * @param object $tool Tool for which the item is created for.
1422
 * @param object $typeconfig The tool configuration.
1423
 * @param object $item Item populated from JSON to be converted to Form form
1424
 *
1425
 * @return stdClass Form config for the item
1426
 */
1427
function content_item_to_form(object $tool, object $typeconfig, object $item): stdClass {
1428
    global $OUTPUT;
1429
 
1430
    $config = new stdClass();
1431
    $config->name = '';
1432
    if (isset($item->title)) {
1433
        $config->name = $item->title;
1434
    }
1435
    if (empty($config->name)) {
1436
        $config->name = $tool->name;
1437
    }
1438
    if (isset($item->text)) {
1439
        $config->introeditor = [
1440
            'text' => $item->text,
1441
            'format' => FORMAT_PLAIN
1442
        ];
1443
    } else {
1444
        $config->introeditor = [
1445
            'text' => '',
1446
            'format' => FORMAT_PLAIN
1447
        ];
1448
    }
1449
    if (isset($item->icon->{'@id'})) {
1450
        $iconurl = new moodle_url($item->icon->{'@id'});
1451
        // Assign item's icon URL to secureicon or icon depending on its scheme.
1452
        if (strtolower($iconurl->get_scheme()) === 'https') {
1453
            $config->secureicon = $iconurl->out(false);
1454
        } else {
1455
            $config->icon = $iconurl->out(false);
1456
        }
1457
    }
1458
    if (isset($item->url)) {
1459
        $url = new moodle_url($item->url);
1460
        $config->toolurl = $url->out(false);
1461
        $config->typeid = 0;
1462
    } else {
1463
        $config->typeid = $tool->id;
1464
    }
1465
    $config->instructorchoiceacceptgrades = LTI_SETTING_NEVER;
1466
    $islti2 = $tool->ltiversion === LTI_VERSION_2;
1467
    if (!$islti2 && isset($typeconfig->lti_acceptgrades)) {
1468
        $acceptgrades = $typeconfig->lti_acceptgrades;
1469
        if ($acceptgrades == LTI_SETTING_ALWAYS) {
1470
            // We create a line item regardless if the definition contains one or not.
1471
            $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
1472
            $config->grade_modgrade_point = 100;
1473
        }
1474
        if ($acceptgrades == LTI_SETTING_DELEGATE || $acceptgrades == LTI_SETTING_ALWAYS) {
1475
            if (isset($item->lineItem)) {
1476
                $lineitem = $item->lineItem;
1477
                $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
1478
                $maxscore = 100;
1479
                if (isset($lineitem->scoreConstraints)) {
1480
                    $sc = $lineitem->scoreConstraints;
1481
                    if (isset($sc->totalMaximum)) {
1482
                        $maxscore = $sc->totalMaximum;
1483
                    } else if (isset($sc->normalMaximum)) {
1484
                        $maxscore = $sc->normalMaximum;
1485
                    }
1486
                }
1487
                $config->grade_modgrade_point = $maxscore;
1488
                $config->lineitemresourceid = '';
1489
                $config->lineitemtag = '';
1490
                $config->lineitemsubreviewurl = '';
1491
                $config->lineitemsubreviewparams = '';
1492
                if (isset($lineitem->assignedActivity) && isset($lineitem->assignedActivity->activityId)) {
1493
                    $config->lineitemresourceid = $lineitem->assignedActivity->activityId?:'';
1494
                }
1495
                if (isset($lineitem->tag)) {
1496
                    $config->lineitemtag = $lineitem->tag?:'';
1497
                }
1498
                if (isset($lineitem->submissionReview)) {
1499
                    $subreview = $lineitem->submissionReview;
1500
                    $config->lineitemsubreviewurl = 'DEFAULT';
1501
                    if (!empty($subreview->url)) {
1502
                        $config->lineitemsubreviewurl = $subreview->url;
1503
                    }
1504
                    if (isset($subreview->custom)) {
1505
                        $config->lineitemsubreviewparams = params_to_string($subreview->custom);
1506
                    }
1507
                }
1508
            }
1509
        }
1510
    }
1511
    $config->instructorchoicesendname = LTI_SETTING_NEVER;
1512
    $config->instructorchoicesendemailaddr = LTI_SETTING_NEVER;
1513
 
1514
    // Since 4.3, the launch container is dictated by the value set in tool configuration and isn't controllable by content items.
1515
    $config->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
1516
 
1517
    if (isset($item->custom)) {
1518
        $config->instructorcustomparameters = params_to_string($item->custom);
1519
    }
1520
 
11 efrain 1521
    // Pass an indicator to the relevant form field.
1 efrain 1522
    $config->selectcontentindicator = $OUTPUT->pix_icon('i/valid', get_string('yes')) . get_string('contentselected', 'mod_lti');
1523
 
1524
    return $config;
1525
}
1526
 
1527
/**
1528
 * Processes the tool provider's response to the ContentItemSelectionRequest and builds the configuration data from the
1529
 * selected content item. This configuration data can be then used when adding a tool into the course.
1530
 *
1531
 * @param int $typeid The tool type ID.
1532
 * @param string $messagetype The value for the lti_message_type parameter.
1533
 * @param string $ltiversion The value for the lti_version parameter.
1534
 * @param string $consumerkey The consumer key.
1535
 * @param string $contentitemsjson The JSON string for the content_items parameter.
1536
 * @return stdClass The array of module information objects.
1537
 * @throws moodle_exception
1538
 * @throws lti\OAuthException
1539
 */
1540
function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiversion, $consumerkey, $contentitemsjson) {
1541
    $tool = lti_get_type($typeid);
1542
    // Validate parameters.
1543
    if (!$tool) {
1544
        throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1545
    }
1546
    // Check lti_message_type. Show debugging if it's not set to ContentItemSelection.
1547
    // No need to throw exceptions for now since lti_message_type does not seem to be used in this processing at the moment.
1548
    if ($messagetype !== 'ContentItemSelection') {
1549
        debugging("lti_message_type is invalid: {$messagetype}. It should be set to 'ContentItemSelection'.",
1550
            DEBUG_DEVELOPER);
1551
    }
1552
 
1553
    // Check LTI versions from our side and the response's side. Show debugging if they don't match.
1554
    // No need to throw exceptions for now since LTI version does not seem to be used in this processing at the moment.
1555
    $expectedversion = $tool->ltiversion;
1556
    $islti2 = ($expectedversion === LTI_VERSION_2);
1557
    if ($ltiversion !== $expectedversion) {
1558
        debugging("lti_version from response does not match the tool's configuration. Tool: {$expectedversion}," .
1559
            " Response: {$ltiversion}", DEBUG_DEVELOPER);
1560
    }
1561
 
1562
    $items = json_decode($contentitemsjson);
1563
    if (empty($items)) {
1564
        throw new moodle_exception('errorinvaliddata', 'mod_lti', '', $contentitemsjson);
1565
    }
1566
    if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'})) {
1567
        throw new moodle_exception('errorinvalidresponseformat', 'mod_lti');
1568
    }
1569
 
1570
    $config = null;
1571
    $items = $items->{'@graph'};
1572
    if (!empty($items)) {
1573
        $typeconfig = lti_get_type_type_config($tool->id);
1574
        if (count($items) == 1) {
1575
            $config = content_item_to_form($tool, $typeconfig, $items[0]);
1576
        } else {
1577
            $multiple = [];
1578
            foreach ($items as $item) {
1579
                $multiple[] = content_item_to_form($tool, $typeconfig, $item);
1580
            }
1581
            $config = new stdClass();
1582
            $config->multiple = $multiple;
1583
        }
1584
    }
1585
    return $config;
1586
}
1587
 
1588
/**
1589
 * Converts the new Deep-Linking format for Content-Items to the old format.
1590
 *
1591
 * @param string $param JSON string representing new Deep-Linking format
1592
 * @return string  JSON representation of content-items
1593
 */
1594
function lti_convert_content_items($param) {
1595
    $items = array();
1596
    $json = json_decode($param);
1597
    if (!empty($json) && is_array($json)) {
1598
        foreach ($json as $item) {
1599
            if (isset($item->type)) {
1600
                $newitem = clone $item;
1601
                switch ($item->type) {
1602
                    case 'ltiResourceLink':
1603
                        $newitem->{'@type'} = 'LtiLinkItem';
1604
                        $newitem->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
1605
                        break;
1606
                    case 'link':
1607
                    case 'rich':
1608
                        $newitem->{'@type'} = 'ContentItem';
1609
                        $newitem->mediaType = 'text/html';
1610
                        break;
1611
                    case 'file':
1612
                        $newitem->{'@type'} = 'FileItem';
1613
                        break;
1614
                }
1615
                unset($newitem->type);
1616
                if (isset($item->html)) {
1617
                    $newitem->text = $item->html;
1618
                    unset($newitem->html);
1619
                }
1620
                if (isset($item->iframe)) {
1621
                    // DeepLinking allows multiple options to be declared as supported.
1622
                    // We favor iframe over new window if both are specified.
1623
                    $newitem->placementAdvice = new stdClass();
1624
                    $newitem->placementAdvice->presentationDocumentTarget = 'iframe';
1625
                    if (isset($item->iframe->width)) {
1626
                        $newitem->placementAdvice->displayWidth = $item->iframe->width;
1627
                    }
1628
                    if (isset($item->iframe->height)) {
1629
                        $newitem->placementAdvice->displayHeight = $item->iframe->height;
1630
                    }
1631
                    unset($newitem->iframe);
1632
                    unset($newitem->window);
1633
                } else if (isset($item->window)) {
1634
                    $newitem->placementAdvice = new stdClass();
1635
                    $newitem->placementAdvice->presentationDocumentTarget = 'window';
1636
                    if (isset($item->window->targetName)) {
1637
                        $newitem->placementAdvice->windowTarget = $item->window->targetName;
1638
                    }
1639
                    if (isset($item->window->width)) {
1640
                        $newitem->placementAdvice->displayWidth = $item->window->width;
1641
                    }
1642
                    if (isset($item->window->height)) {
1643
                        $newitem->placementAdvice->displayHeight = $item->window->height;
1644
                    }
1645
                    unset($newitem->window);
1646
                } else if (isset($item->presentation)) {
1647
                    // This may have been part of an early draft but is not in the final spec
1648
                    // so keeping it around for now in case it's actually been used.
1649
                    $newitem->placementAdvice = new stdClass();
1650
                    if (isset($item->presentation->documentTarget)) {
1651
                        $newitem->placementAdvice->presentationDocumentTarget = $item->presentation->documentTarget;
1652
                    }
1653
                    if (isset($item->presentation->windowTarget)) {
1654
                        $newitem->placementAdvice->windowTarget = $item->presentation->windowTarget;
1655
                    }
1656
                    if (isset($item->presentation->width)) {
1657
                        $newitem->placementAdvice->dislayWidth = $item->presentation->width;
1658
                    }
1659
                    if (isset($item->presentation->height)) {
1660
                        $newitem->placementAdvice->dislayHeight = $item->presentation->height;
1661
                    }
1662
                    unset($newitem->presentation);
1663
                }
1664
                if (isset($item->icon) && isset($item->icon->url)) {
1665
                    $newitem->icon->{'@id'} = $item->icon->url;
1666
                    unset($newitem->icon->url);
1667
                }
1668
                if (isset($item->thumbnail) && isset($item->thumbnail->url)) {
1669
                    $newitem->thumbnail->{'@id'} = $item->thumbnail->url;
1670
                    unset($newitem->thumbnail->url);
1671
                }
1672
                if (isset($item->lineItem)) {
1673
                    unset($newitem->lineItem);
1674
                    $newitem->lineItem = new stdClass();
1675
                    $newitem->lineItem->{'@type'} = 'LineItem';
1676
                    $newitem->lineItem->reportingMethod = 'http://purl.imsglobal.org/ctx/lis/v2p1/Result#totalScore';
1677
                    if (isset($item->lineItem->label)) {
1678
                        $newitem->lineItem->label = $item->lineItem->label;
1679
                    }
1680
                    if (isset($item->lineItem->resourceId)) {
1681
                        $newitem->lineItem->assignedActivity = new stdClass();
1682
                        $newitem->lineItem->assignedActivity->activityId = $item->lineItem->resourceId;
1683
                    }
1684
                    if (isset($item->lineItem->tag)) {
1685
                        $newitem->lineItem->tag = $item->lineItem->tag;
1686
                    }
1687
                    if (isset($item->lineItem->scoreMaximum)) {
1688
                        $newitem->lineItem->scoreConstraints = new stdClass();
1689
                        $newitem->lineItem->scoreConstraints->{'@type'} = 'NumericLimits';
1690
                        $newitem->lineItem->scoreConstraints->totalMaximum = $item->lineItem->scoreMaximum;
1691
                    }
1692
                    if (isset($item->lineItem->submissionReview)) {
1693
                        $newitem->lineItem->submissionReview = $item->lineItem->submissionReview;
1694
                    }
1695
                }
1696
                $items[] = $newitem;
1697
            }
1698
        }
1699
    }
1700
 
1701
    $newitems = new stdClass();
1702
    $newitems->{'@context'} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem';
1703
    $newitems->{'@graph'} = $items;
1704
 
1705
    return json_encode($newitems);
1706
}
1707
 
1708
function lti_get_tool_table($tools, $id) {
1709
    global $OUTPUT;
1710
    $html = '';
1711
 
1712
    $typename = get_string('typename', 'lti');
1713
    $baseurl = get_string('baseurl', 'lti');
1714
    $action = get_string('action', 'lti');
1715
    $createdon = get_string('createdon', 'lti');
1716
 
1717
    if (!empty($tools)) {
1718
        $html .= "
1719
        <div id=\"{$id}_tools_container\" style=\"margin-top:.5em;margin-bottom:.5em\">
1720
            <table id=\"{$id}_tools\">
1721
                <thead>
1722
                    <tr>
1723
                        <th>$typename</th>
1724
                        <th>$baseurl</th>
1725
                        <th>$createdon</th>
1726
                        <th>$action</th>
1727
                    </tr>
1728
                </thead>
1729
        ";
1730
 
1731
        foreach ($tools as $type) {
1732
            $date = userdate($type->timecreated, get_string('strftimedatefullshort', 'core_langconfig'));
1733
            $accept = get_string('accept', 'lti');
1734
            $update = get_string('update', 'lti');
1735
            $delete = get_string('delete', 'lti');
1736
 
1737
            if (empty($type->toolproxyid)) {
1738
                $baseurl = new \moodle_url('/mod/lti/typessettings.php', array(
1739
                        'action' => 'accept',
1740
                        'id' => $type->id,
1741
                        'sesskey' => sesskey(),
1742
                        'tab' => $id
1743
                    ));
1744
                $ref = $type->baseurl;
1745
            } else {
1746
                $baseurl = new \moodle_url('/mod/lti/toolssettings.php', array(
1747
                        'action' => 'accept',
1748
                        'id' => $type->id,
1749
                        'sesskey' => sesskey(),
1750
                        'tab' => $id
1751
                    ));
1752
                $ref = $type->tpname;
1753
            }
1754
 
1755
            $accepthtml = $OUTPUT->action_icon($baseurl,
1756
                    new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null,
1757
                    array('title' => $accept, 'class' => 'editing_accept'));
1758
 
1759
            $deleteaction = 'delete';
1760
 
1761
            if ($type->state == LTI_TOOL_STATE_CONFIGURED) {
1762
                $accepthtml = '';
1763
            }
1764
 
1765
            if ($type->state != LTI_TOOL_STATE_REJECTED) {
1766
                $deleteaction = 'reject';
1767
                $delete = get_string('reject', 'lti');
1768
            }
1769
 
1770
            $updateurl = clone($baseurl);
1771
            $updateurl->param('action', 'update');
1772
            $updatehtml = $OUTPUT->action_icon($updateurl,
1773
                    new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null,
1774
                    array('title' => $update, 'class' => 'editing_update'));
1775
 
1776
            if (($type->state != LTI_TOOL_STATE_REJECTED) || empty($type->toolproxyid)) {
1777
                $deleteurl = clone($baseurl);
1778
                $deleteurl->param('action', $deleteaction);
1779
                $deletehtml = $OUTPUT->action_icon($deleteurl,
1780
                        new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null,
1781
                        array('title' => $delete, 'class' => 'editing_delete'));
1782
            } else {
1783
                $deletehtml = '';
1784
            }
1785
            $html .= "
1786
            <tr>
1787
                <td>
1788
                    {$type->name}
1789
                </td>
1790
                <td>
1791
                    {$ref}
1792
                </td>
1793
                <td>
1794
                    {$date}
1795
                </td>
1796
                <td align=\"center\">
1797
                    {$accepthtml}{$updatehtml}{$deletehtml}
1798
                </td>
1799
            </tr>
1800
            ";
1801
        }
1802
        $html .= '</table></div>';
1803
    } else {
1804
        $html .= get_string('no_' . $id, 'lti');
1805
    }
1806
 
1807
    return $html;
1808
}
1809
 
1810
/**
1811
 * This function builds the tab for a category of tool proxies
1812
 *
1813
 * @param object    $toolproxies    Tool proxy instance objects
1814
 * @param string    $id             Category ID
1815
 *
1816
 * @return string                   HTML for tab
1817
 */
1818
function lti_get_tool_proxy_table($toolproxies, $id) {
1819
    global $OUTPUT;
1820
 
1821
    if (!empty($toolproxies)) {
1822
        $typename = get_string('typename', 'lti');
1823
        $url = get_string('registrationurl', 'lti');
1824
        $action = get_string('action', 'lti');
1825
        $createdon = get_string('createdon', 'lti');
1826
 
1827
        $html = <<< EOD
1828
        <div id="{$id}_tool_proxies_container" style="margin-top: 0.5em; margin-bottom: 0.5em">
1829
            <table id="{$id}_tool_proxies">
1830
                <thead>
1831
                    <tr>
1832
                        <th>{$typename}</th>
1833
                        <th>{$url}</th>
1834
                        <th>{$createdon}</th>
1835
                        <th>{$action}</th>
1836
                    </tr>
1837
                </thead>
1838
EOD;
1839
        foreach ($toolproxies as $toolproxy) {
1840
            $date = userdate($toolproxy->timecreated, get_string('strftimedatefullshort', 'core_langconfig'));
1841
            $accept = get_string('register', 'lti');
1842
            $update = get_string('update', 'lti');
1843
            $delete = get_string('delete', 'lti');
1844
 
1845
            $baseurl = new \moodle_url('/mod/lti/registersettings.php', array(
1846
                    'action' => 'accept',
1847
                    'id' => $toolproxy->id,
1848
                    'sesskey' => sesskey(),
1849
                    'tab' => $id
1850
                ));
1851
 
1852
            $registerurl = new \moodle_url('/mod/lti/register.php', array(
1853
                    'id' => $toolproxy->id,
1854
                    'sesskey' => sesskey(),
1855
                    'tab' => 'tool_proxy'
1856
                ));
1857
 
1858
            $accepthtml = $OUTPUT->action_icon($registerurl,
1859
                    new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null,
1860
                    array('title' => $accept, 'class' => 'editing_accept'));
1861
 
1862
            $deleteaction = 'delete';
1863
 
1864
            if ($toolproxy->state != LTI_TOOL_PROXY_STATE_CONFIGURED) {
1865
                $accepthtml = '';
1866
            }
1867
 
1868
            if (($toolproxy->state == LTI_TOOL_PROXY_STATE_CONFIGURED) || ($toolproxy->state == LTI_TOOL_PROXY_STATE_PENDING)) {
1869
                $delete = get_string('cancel', 'lti');
1870
            }
1871
 
1872
            $updateurl = clone($baseurl);
1873
            $updateurl->param('action', 'update');
1874
            $updatehtml = $OUTPUT->action_icon($updateurl,
1875
                    new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null,
1876
                    array('title' => $update, 'class' => 'editing_update'));
1877
 
1878
            $deleteurl = clone($baseurl);
1879
            $deleteurl->param('action', $deleteaction);
1880
            $deletehtml = $OUTPUT->action_icon($deleteurl,
1881
                    new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null,
1882
                    array('title' => $delete, 'class' => 'editing_delete'));
1883
            $html .= <<< EOD
1884
            <tr>
1885
                <td>
1886
                    {$toolproxy->name}
1887
                </td>
1888
                <td>
1889
                    {$toolproxy->regurl}
1890
                </td>
1891
                <td>
1892
                    {$date}
1893
                </td>
1894
                <td align="center">
1895
                    {$accepthtml}{$updatehtml}{$deletehtml}
1896
                </td>
1897
            </tr>
1898
EOD;
1899
        }
1900
        $html .= '</table></div>';
1901
    } else {
1902
        $html = get_string('no_' . $id, 'lti');
1903
    }
1904
 
1905
    return $html;
1906
}
1907
 
1908
/**
1909
 * Extracts the enabled capabilities into an array, including those implicitly declared in a parameter
1910
 *
1911
 * @param object $tool  Tool instance object
1912
 *
1913
 * @return array List of enabled capabilities
1914
 */
1915
function lti_get_enabled_capabilities($tool) {
1916
    if (!isset($tool)) {
1917
        return array();
1918
    }
1919
    if (!empty($tool->enabledcapability)) {
1920
        $enabledcapabilities = explode("\n", $tool->enabledcapability);
1921
    } else {
1922
        $enabledcapabilities = array();
1923
    }
1924
    if (!empty($tool->parameter)) {
1925
        $paramstr = str_replace("\r\n", "\n", $tool->parameter);
1926
        $paramstr = str_replace("\n\r", "\n", $paramstr);
1927
        $paramstr = str_replace("\r", "\n", $paramstr);
1928
        $params = explode("\n", $paramstr);
1929
        foreach ($params as $param) {
1930
            $pos = strpos($param, '=');
1931
            if (($pos === false) || ($pos < 1)) {
1932
                continue;
1933
            }
1934
            $value = trim(core_text::substr($param, $pos + 1, strlen($param)));
1935
            if (substr($value, 0, 1) == '$') {
1936
                $value = substr($value, 1);
1937
                if (!in_array($value, $enabledcapabilities)) {
1938
                    $enabledcapabilities[] = $value;
1939
                }
1940
            }
1941
        }
1942
    }
1943
    return $enabledcapabilities;
1944
}
1945
 
1946
/**
1947
 * Splits the custom parameters
1948
 *
1949
 * @param string    $customstr      String containing the parameters
1950
 *
1951
 * @return array of custom parameters
1952
 */
1953
function lti_split_parameters($customstr) {
1954
    $customstr = str_replace("\r\n", "\n", $customstr);
1955
    $customstr = str_replace("\n\r", "\n", $customstr);
1956
    $customstr = str_replace("\r", "\n", $customstr);
1957
    $lines = explode("\n", $customstr);  // Or should this split on "/[\n;]/"?
1958
    $retval = array();
1959
    foreach ($lines as $line) {
1960
        $pos = strpos($line, '=');
1961
        if ( $pos === false || $pos < 1 ) {
1962
            continue;
1963
        }
1964
        $key = trim(core_text::substr($line, 0, $pos));
1965
        $val = trim(core_text::substr($line, $pos + 1, strlen($line)));
1966
        $retval[$key] = $val;
1967
    }
1968
    return $retval;
1969
}
1970
 
1971
/**
1972
 * Splits the custom parameters field to the various parameters
1973
 *
1974
 * @param object    $toolproxy      Tool proxy instance object
1975
 * @param object    $tool           Tool instance object
1976
 * @param array     $params         LTI launch parameters
1977
 * @param string    $customstr      String containing the parameters
1978
 * @param boolean   $islti2         True if an LTI 2 tool is being launched
1979
 *
1980
 * @return array of custom parameters
1981
 */
1982
function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2 = false) {
1983
    $splitted = lti_split_parameters($customstr);
1984
    $retval = array();
1985
    foreach ($splitted as $key => $val) {
1986
        $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, $islti2);
1987
        $key2 = lti_map_keyname($key);
1988
        $retval['custom_'.$key2] = $val;
1989
        if (($islti2 || ($tool->ltiversion === LTI_VERSION_1P3)) && ($key != $key2)) {
1990
            $retval['custom_'.$key] = $val;
1991
        }
1992
    }
1993
    return $retval;
1994
}
1995
 
1996
/**
1997
 * Adds the custom parameters to an array
1998
 *
1999
 * @param object    $toolproxy      Tool proxy instance object
2000
 * @param object    $tool           Tool instance object
2001
 * @param array     $params         LTI launch parameters
2002
 * @param array     $parameters     Array containing the parameters
2003
 *
2004
 * @return array    Array of custom parameters
2005
 */
2006
function lti_get_custom_parameters($toolproxy, $tool, $params, $parameters) {
2007
    $retval = array();
2008
    foreach ($parameters as $key => $val) {
2009
        $key2 = lti_map_keyname($key);
2010
        $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, true);
2011
        $retval['custom_'.$key2] = $val;
2012
        if ($key != $key2) {
2013
            $retval['custom_'.$key] = $val;
2014
        }
2015
    }
2016
    return $retval;
2017
}
2018
 
2019
/**
2020
 * Parse a custom parameter to replace any substitution variables
2021
 *
2022
 * @param object    $toolproxy      Tool proxy instance object
2023
 * @param object    $tool           Tool instance object
2024
 * @param array     $params         LTI launch parameters
2025
 * @param string    $value          Custom parameter value
2026
 * @param boolean   $islti2         True if an LTI 2 tool is being launched
2027
 *
2028
 * @return string Parsed value of custom parameter
2029
 */
2030
function lti_parse_custom_parameter($toolproxy, $tool, $params, $value, $islti2) {
2031
    // This is required as {${$valarr[0]}->{$valarr[1]}}" may be using the USER or COURSE var.
2032
    global $USER, $COURSE;
2033
 
2034
    if ($value) {
2035
        if (substr($value, 0, 1) == '\\') {
2036
            $value = substr($value, 1);
2037
        } else if (substr($value, 0, 1) == '$') {
2038
            $value1 = substr($value, 1);
2039
            $enabledcapabilities = lti_get_enabled_capabilities($tool);
2040
            if (!$islti2 || in_array($value1, $enabledcapabilities)) {
2041
                $capabilities = lti_get_capabilities();
2042
                if (array_key_exists($value1, $capabilities)) {
2043
                    $val = $capabilities[$value1];
2044
                    if ($val) {
2045
                        if (substr($val, 0, 1) != '$') {
2046
                            $value = $params[$val];
2047
                        } else {
2048
                            $valarr = explode('->', substr($val, 1), 2);
2049
                            $value = "{${$valarr[0]}->{$valarr[1]}}";
2050
                            $value = str_replace('<br />' , ' ', $value);
2051
                            $value = str_replace('<br>' , ' ', $value);
2052
                            $value = format_string($value);
2053
                        }
2054
                    } else {
2055
                        $value = lti_calculate_custom_parameter($value1);
2056
                    }
2057
                } else {
2058
                    $val = $value;
2059
                    $services = lti_get_services();
2060
                    foreach ($services as $service) {
2061
                        $service->set_tool_proxy($toolproxy);
2062
                        $service->set_type($tool);
2063
                        $value = $service->parse_value($val);
2064
                        if ($val != $value) {
2065
                            break;
2066
                        }
2067
                    }
2068
                }
2069
            }
2070
        }
2071
    }
2072
    return $value;
2073
}
2074
 
2075
/**
2076
 * Calculates the value of a custom parameter that has not been specified earlier
2077
 *
2078
 * @param string    $value          Custom parameter value
2079
 *
2080
 * @return string Calculated value of custom parameter
2081
 */
2082
function lti_calculate_custom_parameter($value) {
2083
    global $USER, $COURSE;
2084
 
2085
    switch ($value) {
2086
        case 'Moodle.Person.userGroupIds':
2087
            return implode(",", groups_get_user_groups($COURSE->id, $USER->id)[0]);
2088
        case 'Context.id.history':
2089
            return implode(",", get_course_history($COURSE));
2090
        case 'CourseSection.timeFrame.begin':
2091
            if (empty($COURSE->startdate)) {
2092
                return "";
2093
            }
2094
            $dt = new DateTime("@$COURSE->startdate", new DateTimeZone('UTC'));
2095
            return $dt->format(DateTime::ATOM);
2096
        case 'CourseSection.timeFrame.end':
2097
            if (empty($COURSE->enddate)) {
2098
                return "";
2099
            }
2100
            $dt = new DateTime("@$COURSE->enddate", new DateTimeZone('UTC'));
2101
            return $dt->format(DateTime::ATOM);
2102
    }
2103
    return null;
2104
}
2105
 
2106
/**
2107
 * Build the history chain for this course using the course originalcourseid.
2108
 *
2109
 * @param object $course course for which the history is returned.
2110
 *
2111
 * @return array ids of the source course in ancestry order, immediate parent 1st.
2112
 */
2113
function get_course_history($course) {
2114
    global $DB;
2115
    $history = [];
2116
    $parentid = $course->originalcourseid;
2117
    while (!empty($parentid) && !in_array($parentid, $history)) {
2118
        $history[] = $parentid;
2119
        $parentid = $DB->get_field('course', 'originalcourseid', array('id' => $parentid));
2120
    }
2121
    return $history;
2122
}
2123
 
2124
/**
2125
 * Used for building the names of the different custom parameters
2126
 *
2127
 * @param string $key   Parameter name
2128
 * @param bool $tolower Do we want to convert the key into lower case?
2129
 * @return string       Processed name
2130
 */
2131
function lti_map_keyname($key, $tolower = true) {
2132
    if ($tolower) {
2133
        $newkey = '';
2134
        $key = core_text::strtolower(trim($key));
2135
        foreach (str_split($key) as $ch) {
2136
            if ( ($ch >= 'a' && $ch <= 'z') || ($ch >= '0' && $ch <= '9') ) {
2137
                $newkey .= $ch;
2138
            } else {
2139
                $newkey .= '_';
2140
            }
2141
        }
2142
    } else {
2143
        $newkey = $key;
2144
    }
2145
    return $newkey;
2146
}
2147
 
2148
/**
2149
 * Gets the IMS role string for the specified user and LTI course module.
2150
 *
2151
 * @param mixed    $user      User object or user id
2152
 * @param int      $cmid      The course module id of the LTI activity
2153
 * @param int      $courseid  The course id of the LTI activity
2154
 * @param boolean  $islti2    True if an LTI 2 tool is being launched
2155
 *
2156
 * @return string A role string suitable for passing with an LTI launch
2157
 */
2158
function lti_get_ims_role($user, $cmid, $courseid, $islti2) {
2159
    $roles = array();
2160
 
2161
    if (empty($cmid)) {
2162
        // If no cmid is passed, check if the user is a teacher in the course
2163
        // This allows other modules to programmatically "fake" a launch without
2164
        // a real LTI instance.
2165
        $context = context_course::instance($courseid);
2166
 
2167
        if (has_capability('moodle/course:manageactivities', $context, $user)) {
2168
            array_push($roles, 'Instructor');
2169
        } else {
2170
            array_push($roles, 'Learner');
2171
        }
2172
    } else {
2173
        $context = context_module::instance($cmid);
2174
 
2175
        if (has_capability('mod/lti:manage', $context)) {
2176
            array_push($roles, 'Instructor');
2177
        } else {
2178
            array_push($roles, 'Learner');
2179
        }
2180
    }
2181
 
2182
    if (!is_role_switched($courseid) && (is_siteadmin($user)) || has_capability('mod/lti:admin', $context)) {
2183
        // Make sure admins do not have the Learner role, then set admin role.
2184
        $roles = array_diff($roles, array('Learner'));
2185
        if (!$islti2) {
2186
            array_push($roles, 'urn:lti:sysrole:ims/lis/Administrator', 'urn:lti:instrole:ims/lis/Administrator');
2187
        } else {
2188
            array_push($roles, 'http://purl.imsglobal.org/vocab/lis/v2/person#Administrator');
2189
        }
2190
    }
2191
 
2192
    return join(',', $roles);
2193
}
2194
 
2195
/**
2196
 * Returns configuration details for the tool
2197
 *
2198
 * @param int $typeid   Basic LTI tool typeid
2199
 *
2200
 * @return array        Tool Configuration
2201
 */
2202
function lti_get_type_config($typeid) {
2203
    global $DB;
2204
 
2205
    $query = "SELECT name, value
2206
                FROM {lti_types_config}
2207
               WHERE typeid = :typeid1
2208
           UNION ALL
2209
              SELECT 'toolurl' AS name, baseurl AS value
2210
                FROM {lti_types}
2211
               WHERE id = :typeid2
2212
           UNION ALL
2213
              SELECT 'icon' AS name, icon AS value
2214
                FROM {lti_types}
2215
               WHERE id = :typeid3
2216
           UNION ALL
2217
              SELECT 'secureicon' AS name, secureicon AS value
2218
                FROM {lti_types}
2219
               WHERE id = :typeid4";
2220
 
2221
    $typeconfig = array();
2222
    $configs = $DB->get_records_sql($query,
2223
        array('typeid1' => $typeid, 'typeid2' => $typeid, 'typeid3' => $typeid, 'typeid4' => $typeid));
2224
 
2225
    if (!empty($configs)) {
2226
        foreach ($configs as $config) {
2227
            $typeconfig[$config->name] = $config->value;
2228
        }
2229
    }
2230
 
2231
    return $typeconfig;
2232
}
2233
 
2234
function lti_get_tools_by_url($url, $state, $courseid = null) {
2235
    $domain = lti_get_domain_from_url($url);
2236
 
2237
    return lti_get_tools_by_domain($domain, $state, $courseid);
2238
}
2239
 
2240
function lti_get_tools_by_domain($domain, $state = null, $courseid = null) {
2241
    global $DB, $SITE;
2242
 
2243
    $statefilter = '';
2244
    $coursefilter = '';
2245
 
2246
    if ($state) {
2247
        $statefilter = 'AND t.state = :state';
2248
    }
2249
 
2250
    if ($courseid && $courseid != $SITE->id) {
2251
        $coursefilter = 'OR t.course = :courseid';
2252
    }
2253
 
2254
    $coursecategory = $DB->get_field('course', 'category', ['id' => $courseid]);
2255
    $query = "SELECT t.*
2256
                FROM {lti_types} t
2257
           LEFT JOIN {lti_types_categories} tc on t.id = tc.typeid
2258
               WHERE t.tooldomain = :tooldomain
2259
                 AND (t.course = :siteid $coursefilter)
2260
                 $statefilter
2261
                 AND (tc.id IS NULL OR tc.categoryid = :categoryid)";
2262
 
2263
    return $DB->get_records_sql($query, [
2264
            'courseid' => $courseid,
2265
            'siteid' => $SITE->id,
2266
            'tooldomain' => $domain,
2267
            'state' => $state,
2268
            'categoryid' => $coursecategory
2269
        ]);
2270
}
2271
 
2272
/**
2273
 * Returns all basicLTI tools configured by the administrator
2274
 *
2275
 * @param int $course
2276
 *
2277
 * @return array
2278
 */
2279
function lti_filter_get_types($course) {
2280
    global $DB;
2281
 
2282
    if (!empty($course)) {
2283
        $where = "WHERE t.course = :course";
2284
        $params = array('course' => $course);
2285
    } else {
2286
        $where = '';
2287
        $params = array();
2288
    }
2289
    $query = "SELECT t.id, t.name, t.baseurl, t.state, t.toolproxyid, t.timecreated, tp.name tpname
2290
                FROM {lti_types} t LEFT OUTER JOIN {lti_tool_proxies} tp ON t.toolproxyid = tp.id
2291
                {$where}";
2292
    return $DB->get_records_sql($query, $params);
2293
}
2294
 
2295
/**
2296
 * Given an array of tools, filter them based on their state
2297
 *
2298
 * @param array $tools An array of lti_types records
2299
 * @param int $state One of the LTI_TOOL_STATE_* constants
2300
 * @return array
2301
 */
2302
function lti_filter_tool_types(array $tools, $state) {
2303
    $return = array();
2304
    foreach ($tools as $key => $tool) {
2305
        if ($tool->state == $state) {
2306
            $return[$key] = $tool;
2307
        }
2308
    }
2309
    return $return;
2310
}
2311
 
2312
/**
2313
 * Returns all lti types visible in this course
2314
 *
2315
 * @deprecated since Moodle 4.3
2316
 * @param int $courseid The id of the course to retieve types for
2317
 * @param array $coursevisible options for 'coursevisible' field,
2318
 *        default [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER]
2319
 * @return stdClass[] All the lti types visible in the given course
2320
 */
2321
function lti_get_lti_types_by_course($courseid, $coursevisible = null) {
2322
    debugging(__FUNCTION__ . '() is deprecated. Please use \mod_lti\local\types_helper::get_lti_types_by_course() instead.',
2323
        DEBUG_DEVELOPER);
2324
 
2325
    global $USER;
2326
    return \mod_lti\local\types_helper::get_lti_types_by_course($courseid, $USER->id, $coursevisible ?? []);
2327
}
2328
 
2329
/**
2330
 * Returns tool types for lti add instance and edit page
2331
 *
2332
 * @return array Array of lti types
2333
 */
2334
function lti_get_types_for_add_instance() {
2335
    global $COURSE, $USER;
2336
 
2337
    // Always return the 'manual' type option, despite manual config being deprecated, so that we have it for legacy instances.
2338
    $types = [(object) ['name' => get_string('automatic', 'lti'), 'course' => 0, 'toolproxyid' => null]];
2339
 
2340
    $preconfiguredtypes = \mod_lti\local\types_helper::get_lti_types_by_course($COURSE->id, $USER->id);
2341
    foreach ($preconfiguredtypes as $type) {
2342
        $types[$type->id] = $type;
2343
    }
2344
 
2345
    return $types;
2346
}
2347
 
2348
/**
2349
 * Returns a list of configured types in the given course
2350
 *
2351
 * @param int $courseid The id of the course to retieve types for
2352
 * @param int $sectionreturn section to return to for forming the URLs
2353
 * @return array Array of lti types. Each element is object with properties: name, title, icon, help, helplink, link
2354
 */
2355
function lti_get_configured_types($courseid, $sectionreturn = 0) {
2356
    global $OUTPUT, $USER;
2357
    $types = [];
2358
    $preconfiguredtypes = \mod_lti\local\types_helper::get_lti_types_by_course($courseid, $USER->id,
2359
        [LTI_COURSEVISIBLE_ACTIVITYCHOOSER]);
2360
 
2361
    foreach ($preconfiguredtypes as $ltitype) {
2362
        $type           = new stdClass();
2363
        $type->id       = $ltitype->id;
2364
        $type->modclass = MOD_CLASS_ACTIVITY;
2365
        $type->name     = 'lti_type_' . $ltitype->id;
2366
        // Clean the name. We don't want tags here.
2367
        $type->title    = clean_param($ltitype->name, PARAM_NOTAGS);
2368
        $trimmeddescription = trim($ltitype->description ?? '');
2369
        if ($trimmeddescription != '') {
2370
            // Clean the description. We don't want tags here.
2371
            $type->help     = clean_param($trimmeddescription, PARAM_NOTAGS);
2372
            $type->helplink = get_string('modulename_shortcut_link', 'lti');
2373
        }
2374
 
2375
        $iconurl = get_tool_type_icon_url($ltitype);
2376
        $iconclass = '';
2377
        if ($iconurl !== $OUTPUT->image_url('monologo', 'lti')->out()) {
2378
            // Do not filter the icon if it is not the default LTI activity icon.
2379
            $iconclass = 'nofilter';
2380
        }
2381
        $type->icon = html_writer::empty_tag('img', ['src' => $iconurl, 'alt' => '', 'class' => "icon $iconclass"]);
2382
 
2383
        $params = [
2384
            'add' => 'lti',
2385
            'return' => 0,
2386
            'course' => $courseid,
2387
            'typeid' => $ltitype->id,
2388
        ];
2389
        if (!is_null($sectionreturn)) {
2390
            $params['sr'] = $sectionreturn;
2391
        }
2392
        $type->link = new moodle_url('/course/modedit.php', $params);
2393
        $types[] = $type;
2394
    }
2395
    return $types;
2396
}
2397
 
2398
function lti_get_domain_from_url($url) {
2399
    $matches = array();
2400
 
2401
    if (preg_match(LTI_URL_DOMAIN_REGEX, $url ?? '', $matches)) {
2402
        return $matches[1];
2403
    }
2404
}
2405
 
2406
function lti_get_tool_by_url_match($url, $courseid = null, $state = LTI_TOOL_STATE_CONFIGURED) {
2407
    $possibletools = lti_get_tools_by_url($url, $state, $courseid);
2408
 
2409
    return lti_get_best_tool_by_url($url, $possibletools, $courseid);
2410
}
2411
 
2412
function lti_get_url_thumbprint($url) {
2413
    // Parse URL requires a schema otherwise everything goes into 'path'.  Fixed 5.4.7 or later.
2414
    if (preg_match('/https?:\/\//', $url) !== 1) {
2415
        $url = 'http://'.$url;
2416
    }
2417
    $urlparts = parse_url(strtolower($url));
2418
    if (!isset($urlparts['path'])) {
2419
        $urlparts['path'] = '';
2420
    }
2421
 
2422
    if (!isset($urlparts['query'])) {
2423
        $urlparts['query'] = '';
2424
    }
2425
 
2426
    if (!isset($urlparts['host'])) {
2427
        $urlparts['host'] = '';
2428
    }
2429
 
2430
    if (substr($urlparts['host'], 0, 4) === 'www.') {
2431
        $urlparts['host'] = substr($urlparts['host'], 4);
2432
    }
2433
 
2434
    $urllower = $urlparts['host'] . '/' . $urlparts['path'];
2435
 
2436
    if ($urlparts['query'] != '') {
2437
        $urllower .= '?' . $urlparts['query'];
2438
    }
2439
 
2440
    return $urllower;
2441
}
2442
 
2443
function lti_get_best_tool_by_url($url, $tools, $courseid = null) {
2444
    if (count($tools) === 0) {
2445
        return null;
2446
    }
2447
 
2448
    $urllower = lti_get_url_thumbprint($url);
2449
 
2450
    foreach ($tools as $tool) {
2451
        $tool->_matchscore = 0;
2452
 
2453
        $toolbaseurllower = lti_get_url_thumbprint($tool->baseurl);
2454
 
2455
        if ($urllower === $toolbaseurllower) {
2456
            // 100 points for exact thumbprint match.
2457
            $tool->_matchscore += 100;
2458
        } else if (substr($urllower, 0, strlen($toolbaseurllower)) === $toolbaseurllower) {
2459
            // 50 points if tool thumbprint starts with the base URL thumbprint.
2460
            $tool->_matchscore += 50;
2461
        }
2462
 
2463
        // Prefer course tools over site tools.
2464
        if (!empty($courseid)) {
2465
            // Minus 10 points for not matching the course id (global tools).
2466
            if ($tool->course != $courseid) {
2467
                $tool->_matchscore -= 10;
2468
            }
2469
        }
2470
    }
2471
 
2472
    $bestmatch = array_reduce($tools, function($value, $tool) {
2473
        if ($tool->_matchscore > $value->_matchscore) {
2474
            return $tool;
2475
        } else {
2476
            return $value;
2477
        }
2478
 
2479
    }, (object)array('_matchscore' => -1));
2480
 
2481
    // None of the tools are suitable for this URL.
2482
    if ($bestmatch->_matchscore <= 0) {
2483
        return null;
2484
    }
2485
 
2486
    return $bestmatch;
2487
}
2488
 
2489
function lti_get_shared_secrets_by_key($key) {
2490
    global $DB;
2491
 
2492
    // Look up the shared secret for the specified key in both the types_config table (for configured tools)
2493
    // And in the lti resource table for ad-hoc tools.
2494
    $lti13 = LTI_VERSION_1P3;
2495
    $query = "SELECT " . $DB->sql_compare_text('t2.value', 256) . " AS value
2496
                FROM {lti_types_config} t1
2497
                JOIN {lti_types_config} t2 ON t1.typeid = t2.typeid
2498
                JOIN {lti_types} type ON t2.typeid = type.id
2499
              WHERE t1.name = 'resourcekey'
2500
                AND " . $DB->sql_compare_text('t1.value', 256) . " = :key1
2501
                AND t2.name = 'password'
2502
                AND type.state = :configured1
2503
                AND type.ltiversion <> :ltiversion
2504
               UNION
2505
              SELECT tp.secret AS value
2506
                FROM {lti_tool_proxies} tp
2507
                JOIN {lti_types} t ON tp.id = t.toolproxyid
2508
              WHERE tp.guid = :key2
2509
                AND t.state = :configured2
2510
               UNION
2511
              SELECT password AS value
2512
               FROM {lti}
2513
              WHERE resourcekey = :key3";
2514
 
2515
    $sharedsecrets = $DB->get_records_sql($query, array('configured1' => LTI_TOOL_STATE_CONFIGURED, 'ltiversion' => $lti13,
2516
        'configured2' => LTI_TOOL_STATE_CONFIGURED, 'key1' => $key, 'key2' => $key, 'key3' => $key));
2517
 
2518
    $values = array_map(function($item) {
2519
        return $item->value;
2520
    }, $sharedsecrets);
2521
 
2522
    // There should really only be one shared secret per key. But, we can't prevent
2523
    // more than one getting entered. For instance, if the same key is used for two tool providers.
2524
    return $values;
2525
}
2526
 
2527
/**
2528
 * Delete a Basic LTI configuration
2529
 *
2530
 * @param int $id   Configuration id
2531
 */
2532
function lti_delete_type($id) {
2533
    global $DB;
2534
 
2535
    // We should probably just copy the launch URL to the tool instances in this case... using a single query.
2536
    /*
2537
    $instances = $DB->get_records('lti', array('typeid' => $id));
2538
    foreach ($instances as $instance) {
2539
        $instance->typeid = 0;
2540
        $DB->update_record('lti', $instance);
2541
    }*/
2542
 
2543
    $DB->delete_records('lti_types', array('id' => $id));
2544
    $DB->delete_records('lti_types_config', array('typeid' => $id));
2545
    $DB->delete_records('lti_types_categories', array('typeid' => $id));
2546
}
2547
 
2548
function lti_set_state_for_type($id, $state) {
2549
    global $DB;
2550
 
2551
    $DB->update_record('lti_types', (object)array('id' => $id, 'state' => $state));
2552
}
2553
 
2554
/**
2555
 * Transforms a basic LTI object to an array
2556
 *
2557
 * @param object $ltiobject    Basic LTI object
2558
 *
2559
 * @return array Basic LTI configuration details
2560
 */
2561
function lti_get_config($ltiobject) {
2562
    $typeconfig = (array)$ltiobject;
2563
    $additionalconfig = lti_get_type_config($ltiobject->typeid);
2564
    $typeconfig = array_merge($typeconfig, $additionalconfig);
2565
    return $typeconfig;
2566
}
2567
 
2568
/**
2569
 *
2570
 * Generates some of the tool configuration based on the instance details
2571
 *
2572
 * @param int $id
2573
 *
2574
 * @return object configuration
2575
 *
2576
 */
2577
function lti_get_type_config_from_instance($id) {
2578
    global $DB;
2579
 
2580
    $instance = $DB->get_record('lti', array('id' => $id));
2581
    $config = lti_get_config($instance);
2582
 
2583
    $type = new \stdClass();
2584
    $type->lti_fix = $id;
2585
    if (isset($config['toolurl'])) {
2586
        $type->lti_toolurl = $config['toolurl'];
2587
    }
2588
    if (isset($config['instructorchoicesendname'])) {
2589
        $type->lti_sendname = $config['instructorchoicesendname'];
2590
    }
2591
    if (isset($config['instructorchoicesendemailaddr'])) {
2592
        $type->lti_sendemailaddr = $config['instructorchoicesendemailaddr'];
2593
    }
2594
    if (isset($config['instructorchoiceacceptgrades'])) {
2595
        $type->lti_acceptgrades = $config['instructorchoiceacceptgrades'];
2596
    }
2597
    if (isset($config['instructorchoiceallowroster'])) {
2598
        $type->lti_allowroster = $config['instructorchoiceallowroster'];
2599
    }
2600
 
2601
    if (isset($config['instructorcustomparameters'])) {
2602
        $type->lti_allowsetting = $config['instructorcustomparameters'];
2603
    }
2604
    return $type;
2605
}
2606
 
2607
/**
2608
 * Generates some of the tool configuration based on the admin configuration details
2609
 *
2610
 * @param int $id
2611
 *
2612
 * @return stdClass Configuration details
2613
 */
2614
function lti_get_type_type_config($id) {
2615
    global $DB;
2616
 
2617
    $basicltitype = $DB->get_record('lti_types', array('id' => $id));
2618
    $config = lti_get_type_config($id);
2619
 
2620
    $type = new \stdClass();
2621
 
2622
    $type->lti_typename = $basicltitype->name;
2623
 
2624
    $type->typeid = $basicltitype->id;
2625
 
2626
    $type->course = $basicltitype->course;
2627
 
2628
    $type->toolproxyid = $basicltitype->toolproxyid;
2629
 
2630
    $type->lti_toolurl = $basicltitype->baseurl;
2631
 
2632
    $type->lti_ltiversion = $basicltitype->ltiversion;
2633
 
2634
    $type->lti_clientid = $basicltitype->clientid;
2635
    $type->lti_clientid_disabled = $type->lti_clientid;
2636
 
2637
    $type->lti_description = $basicltitype->description;
2638
 
2639
    $type->lti_parameters = $basicltitype->parameter;
2640
 
2641
    $type->lti_icon = $basicltitype->icon;
2642
 
2643
    $type->lti_secureicon = $basicltitype->secureicon;
2644
 
2645
    if (isset($config['resourcekey'])) {
2646
        $type->lti_resourcekey = $config['resourcekey'];
2647
    }
2648
    if (isset($config['password'])) {
2649
        $type->lti_password = $config['password'];
2650
    }
2651
    if (isset($config['publickey'])) {
2652
        $type->lti_publickey = $config['publickey'];
2653
    }
2654
    if (isset($config['publickeyset'])) {
2655
        $type->lti_publickeyset = $config['publickeyset'];
2656
    }
2657
    if (isset($config['keytype'])) {
2658
        $type->lti_keytype = $config['keytype'];
2659
    }
2660
    if (isset($config['initiatelogin'])) {
2661
        $type->lti_initiatelogin = $config['initiatelogin'];
2662
    }
2663
    if (isset($config['redirectionuris'])) {
2664
        $type->lti_redirectionuris = $config['redirectionuris'];
2665
    }
2666
 
2667
    if (isset($config['sendname'])) {
2668
        $type->lti_sendname = $config['sendname'];
2669
    }
2670
    if (isset($config['instructorchoicesendname'])) {
2671
        $type->lti_instructorchoicesendname = $config['instructorchoicesendname'];
2672
    }
2673
    if (isset($config['sendemailaddr'])) {
2674
        $type->lti_sendemailaddr = $config['sendemailaddr'];
2675
    }
2676
    if (isset($config['instructorchoicesendemailaddr'])) {
2677
        $type->lti_instructorchoicesendemailaddr = $config['instructorchoicesendemailaddr'];
2678
    }
2679
    if (isset($config['acceptgrades'])) {
2680
        $type->lti_acceptgrades = $config['acceptgrades'];
2681
    }
2682
    if (isset($config['instructorchoiceacceptgrades'])) {
2683
        $type->lti_instructorchoiceacceptgrades = $config['instructorchoiceacceptgrades'];
2684
    }
2685
    if (isset($config['allowroster'])) {
2686
        $type->lti_allowroster = $config['allowroster'];
2687
    }
2688
    if (isset($config['instructorchoiceallowroster'])) {
2689
        $type->lti_instructorchoiceallowroster = $config['instructorchoiceallowroster'];
2690
    }
2691
 
2692
    if (isset($config['customparameters'])) {
2693
        $type->lti_customparameters = $config['customparameters'];
2694
    }
2695
 
2696
    if (isset($config['forcessl'])) {
2697
        $type->lti_forcessl = $config['forcessl'];
2698
    }
2699
 
2700
    if (isset($config['organizationid_default'])) {
2701
        $type->lti_organizationid_default = $config['organizationid_default'];
2702
    } else {
2703
        // Tool was configured before this option was available and the default then was host.
2704
        $type->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEHOST;
2705
    }
2706
    if (isset($config['organizationid'])) {
2707
        $type->lti_organizationid = $config['organizationid'];
2708
    }
2709
    if (isset($config['organizationurl'])) {
2710
        $type->lti_organizationurl = $config['organizationurl'];
2711
    }
2712
    if (isset($config['organizationdescr'])) {
2713
        $type->lti_organizationdescr = $config['organizationdescr'];
2714
    }
2715
    if (isset($config['launchcontainer'])) {
2716
        $type->lti_launchcontainer = $config['launchcontainer'];
2717
    }
2718
 
2719
    if (isset($config['coursevisible'])) {
2720
        $type->lti_coursevisible = $config['coursevisible'];
2721
    }
2722
 
2723
    if (isset($config['contentitem'])) {
2724
        $type->lti_contentitem = $config['contentitem'];
2725
    }
2726
 
2727
    if (isset($config['toolurl_ContentItemSelectionRequest'])) {
2728
        $type->lti_toolurl_ContentItemSelectionRequest = $config['toolurl_ContentItemSelectionRequest'];
2729
    }
2730
 
2731
    if (isset($config['debuglaunch'])) {
2732
        $type->lti_debuglaunch = $config['debuglaunch'];
2733
    }
2734
 
2735
    if (isset($config['module_class_type'])) {
2736
        $type->lti_module_class_type = $config['module_class_type'];
2737
    }
2738
 
2739
    // Get the parameters from the LTI services.
2740
    foreach ($config as $name => $value) {
2741
        if (strpos($name, 'ltiservice_') === 0) {
2742
            $type->{$name} = $config[$name];
2743
        }
2744
    }
2745
 
2746
    return $type;
2747
}
2748
 
2749
function lti_prepare_type_for_save($type, $config) {
2750
    if (isset($config->lti_toolurl)) {
2751
        $type->baseurl = $config->lti_toolurl;
2752
        if (isset($config->lti_tooldomain)) {
2753
            $type->tooldomain = $config->lti_tooldomain;
2754
        } else {
2755
            $type->tooldomain = lti_get_domain_from_url($config->lti_toolurl);
2756
        }
2757
    }
2758
    if (isset($config->lti_description)) {
2759
        $type->description = $config->lti_description;
2760
    }
2761
    if (isset($config->lti_typename)) {
2762
        $type->name = $config->lti_typename;
2763
    }
2764
    if (isset($config->lti_ltiversion)) {
2765
        $type->ltiversion = $config->lti_ltiversion;
2766
    }
2767
    if (isset($config->lti_clientid)) {
2768
        $type->clientid = $config->lti_clientid;
2769
    }
2770
    if ((!empty($type->ltiversion) && $type->ltiversion === LTI_VERSION_1P3) && empty($type->clientid)) {
2771
        $type->clientid = registration_helper::get()->new_clientid();
2772
    } else if (empty($type->clientid)) {
2773
        $type->clientid = null;
2774
    }
2775
    if (isset($config->lti_coursevisible)) {
2776
        $type->coursevisible = $config->lti_coursevisible;
2777
    }
2778
 
2779
    if (isset($config->lti_icon)) {
2780
        $type->icon = $config->lti_icon;
2781
    }
2782
    if (isset($config->lti_secureicon)) {
2783
        $type->secureicon = $config->lti_secureicon;
2784
    }
2785
 
2786
    $type->forcessl = !empty($config->lti_forcessl) ? $config->lti_forcessl : 0;
2787
    $config->lti_forcessl = $type->forcessl;
2788
    if (isset($config->lti_contentitem)) {
2789
        $type->contentitem = !empty($config->lti_contentitem) ? $config->lti_contentitem : 0;
2790
        $config->lti_contentitem = $type->contentitem;
2791
    }
2792
    if (isset($config->lti_toolurl_ContentItemSelectionRequest)) {
2793
        if (!empty($config->lti_toolurl_ContentItemSelectionRequest)) {
2794
            $type->toolurl_ContentItemSelectionRequest = $config->lti_toolurl_ContentItemSelectionRequest;
2795
        } else {
2796
            $type->toolurl_ContentItemSelectionRequest = '';
2797
        }
2798
        $config->lti_toolurl_ContentItemSelectionRequest = $type->toolurl_ContentItemSelectionRequest;
2799
    }
2800
 
2801
    $type->timemodified = time();
2802
 
2803
    unset ($config->lti_typename);
2804
    unset ($config->lti_toolurl);
2805
    unset ($config->lti_description);
2806
    unset ($config->lti_ltiversion);
2807
    unset ($config->lti_clientid);
2808
    unset ($config->lti_icon);
2809
    unset ($config->lti_secureicon);
2810
}
2811
 
2812
function lti_update_type($type, $config) {
2813
    global $DB, $CFG;
2814
 
2815
    lti_prepare_type_for_save($type, $config);
2816
 
2817
    if (lti_request_is_using_ssl() && !empty($type->secureicon)) {
2818
        $clearcache = !isset($config->oldicon) || ($config->oldicon !== $type->secureicon);
2819
    } else {
2820
        $clearcache = isset($type->icon) && (!isset($config->oldicon) || ($config->oldicon !== $type->icon));
2821
    }
2822
    unset($config->oldicon);
2823
 
2824
    if ($DB->update_record('lti_types', $type)) {
2825
        foreach ($config as $key => $value) {
2826
            if (substr($key, 0, 4) == 'lti_' && !is_null($value)) {
2827
                $record = new \StdClass();
2828
                $record->typeid = $type->id;
2829
                $record->name = substr($key, 4);
2830
                $record->value = $value;
2831
                lti_update_config($record);
2832
            }
2833
            if (substr($key, 0, 11) == 'ltiservice_' && !is_null($value)) {
2834
                $record = new \StdClass();
2835
                $record->typeid = $type->id;
2836
                $record->name = $key;
2837
                $record->value = $value;
2838
                lti_update_config($record);
2839
            }
2840
        }
2841
        if (isset($type->toolproxyid) && $type->ltiversion === LTI_VERSION_1P3) {
2842
            // We need to remove the tool proxy for this tool to function under 1.3.
2843
            $toolproxyid = $type->toolproxyid;
2844
            $DB->delete_records('lti_tool_settings', array('toolproxyid' => $toolproxyid));
2845
            $DB->delete_records('lti_tool_proxies', array('id' => $toolproxyid));
2846
            $type->toolproxyid = null;
2847
            $DB->update_record('lti_types', $type);
2848
        }
2849
        $DB->delete_records('lti_types_categories', ['typeid' => $type->id]);
2850
        if (isset($config->lti_coursecategories) && !empty($config->lti_coursecategories)) {
2851
            lti_type_add_categories($type->id, $config->lti_coursecategories);
2852
        }
2853
        require_once($CFG->libdir.'/modinfolib.php');
2854
        if ($clearcache) {
2855
            $sql = "SELECT cm.id, cm.course
2856
                      FROM {course_modules} cm
2857
                      JOIN {modules} m ON cm.module = m.id
2858
                      JOIN {lti} l ON l.course = cm.course
2859
                     WHERE m.name = :name AND l.typeid = :typeid";
2860
 
2861
            $rs = $DB->get_recordset_sql($sql, ['name' => 'lti', 'typeid' => $type->id]);
2862
 
2863
            $courseids = [];
2864
            foreach ($rs as $record) {
2865
                $courseids[] = $record->course;
2866
                \course_modinfo::purge_course_module_cache($record->course, $record->id);
2867
            }
2868
            $rs->close();
2869
            $courseids = array_unique($courseids);
2870
            foreach ($courseids as $courseid) {
2871
                rebuild_course_cache($courseid, false, true);
2872
            }
2873
        }
2874
    }
2875
}
2876
 
2877
/**
2878
 * Add LTI Type course category.
2879
 *
2880
 * @param int $typeid
2881
 * @param string $lticoursecategories Comma separated list of course categories.
2882
 * @return void
2883
 */
2884
function lti_type_add_categories(int $typeid, string $lticoursecategories = ''): void {
2885
    global $DB;
2886
    $coursecategories = explode(',', $lticoursecategories);
2887
    foreach ($coursecategories as $coursecategory) {
2888
        $DB->insert_record('lti_types_categories', ['typeid' => $typeid, 'categoryid' => $coursecategory]);
2889
    }
2890
}
2891
 
2892
function lti_add_type($type, $config) {
2893
    global $USER, $SITE, $DB;
2894
 
2895
    lti_prepare_type_for_save($type, $config);
2896
 
2897
    if (!isset($type->state)) {
2898
        $type->state = LTI_TOOL_STATE_PENDING;
2899
    }
2900
 
2901
    if (!isset($type->ltiversion)) {
2902
        $type->ltiversion = LTI_VERSION_1;
2903
    }
2904
 
2905
    if (!isset($type->timecreated)) {
2906
        $type->timecreated = time();
2907
    }
2908
 
2909
    if (!isset($type->createdby)) {
2910
        $type->createdby = $USER->id;
2911
    }
2912
 
2913
    if (!isset($type->course)) {
2914
        $type->course = $SITE->id;
2915
    }
2916
 
2917
    // Create a salt value to be used for signing passed data to extension services
2918
    // The outcome service uses the service salt on the instance. This can be used
2919
    // for communication with services not related to a specific LTI instance.
2920
    $config->lti_servicesalt = uniqid('', true);
2921
 
2922
    $id = $DB->insert_record('lti_types', $type);
2923
 
2924
    if ($id) {
2925
        foreach ($config as $key => $value) {
2926
            if (!is_null($value)) {
2927
                if (substr($key, 0, 4) === 'lti_') {
2928
                    $fieldname = substr($key, 4);
2929
                } else if (substr($key, 0, 11) !== 'ltiservice_') {
2930
                    continue;
2931
                } else {
2932
                    $fieldname = $key;
2933
                }
2934
 
2935
                $record = new \StdClass();
2936
                $record->typeid = $id;
2937
                $record->name = $fieldname;
2938
                $record->value = $value;
2939
 
2940
                lti_add_config($record);
2941
            }
2942
        }
2943
        if (isset($config->lti_coursecategories) && !empty($config->lti_coursecategories)) {
2944
            lti_type_add_categories($id, $config->lti_coursecategories);
2945
        }
2946
    }
2947
 
2948
    return $id;
2949
}
2950
 
2951
/**
2952
 * Given an array of tool proxies, filter them based on their state
2953
 *
2954
 * @param array $toolproxies An array of lti_tool_proxies records
2955
 * @param int $state One of the LTI_TOOL_PROXY_STATE_* constants
2956
 *
2957
 * @return array
2958
 */
2959
function lti_filter_tool_proxy_types(array $toolproxies, $state) {
2960
    $return = array();
2961
    foreach ($toolproxies as $key => $toolproxy) {
2962
        if ($toolproxy->state == $state) {
2963
            $return[$key] = $toolproxy;
2964
        }
2965
    }
2966
    return $return;
2967
}
2968
 
2969
/**
2970
 * Get the tool proxy instance given its GUID
2971
 *
2972
 * @param string  $toolproxyguid   Tool proxy GUID value
2973
 *
2974
 * @return object
2975
 */
2976
function lti_get_tool_proxy_from_guid($toolproxyguid) {
2977
    global $DB;
2978
 
2979
    $toolproxy = $DB->get_record('lti_tool_proxies', array('guid' => $toolproxyguid));
2980
 
2981
    return $toolproxy;
2982
}
2983
 
2984
/**
2985
 * Get the tool proxy instance given its registration URL
2986
 *
2987
 * @param string $regurl Tool proxy registration URL
2988
 *
2989
 * @return array The record of the tool proxy with this url
2990
 */
2991
function lti_get_tool_proxies_from_registration_url($regurl) {
2992
    global $DB;
2993
 
2994
    return $DB->get_records_sql(
2995
        'SELECT * FROM {lti_tool_proxies}
2996
        WHERE '.$DB->sql_compare_text('regurl', 256).' = :regurl',
2997
        array('regurl' => $regurl)
2998
    );
2999
}
3000
 
3001
/**
3002
 * Generates some of the tool proxy configuration based on the admin configuration details
3003
 *
3004
 * @param int $id
3005
 *
3006
 * @return mixed Tool Proxy details
3007
 */
3008
function lti_get_tool_proxy($id) {
3009
    global $DB;
3010
 
3011
    $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $id));
3012
    return $toolproxy;
3013
}
3014
 
3015
/**
3016
 * Returns lti tool proxies.
3017
 *
3018
 * @param bool $orphanedonly Only retrieves tool proxies that have no type associated with them
3019
 * @return array of basicLTI types
3020
 */
3021
function lti_get_tool_proxies($orphanedonly) {
3022
    global $DB;
3023
 
3024
    if ($orphanedonly) {
3025
        $usedproxyids = array_values($DB->get_fieldset_select('lti_types', 'toolproxyid', 'toolproxyid IS NOT NULL'));
3026
        $proxies = $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC');
3027
        foreach ($proxies as $key => $value) {
3028
            if (in_array($value->id, $usedproxyids)) {
3029
                unset($proxies[$key]);
3030
            }
3031
        }
3032
        return $proxies;
3033
    } else {
3034
        return $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC');
3035
    }
3036
}
3037
 
3038
/**
3039
 * Generates some of the tool proxy configuration based on the admin configuration details
3040
 *
3041
 * @param int $id
3042
 *
3043
 * @return mixed  Tool Proxy details
3044
 */
3045
function lti_get_tool_proxy_config($id) {
3046
    $toolproxy = lti_get_tool_proxy($id);
3047
 
3048
    $tp = new \stdClass();
3049
    $tp->lti_registrationname = $toolproxy->name;
3050
    $tp->toolproxyid = $toolproxy->id;
3051
    $tp->state = $toolproxy->state;
3052
    $tp->lti_registrationurl = $toolproxy->regurl;
3053
    $tp->lti_capabilities = explode("\n", $toolproxy->capabilityoffered);
3054
    $tp->lti_services = explode("\n", $toolproxy->serviceoffered);
3055
 
3056
    return $tp;
3057
}
3058
 
3059
/**
3060
 * Update the database with a tool proxy instance
3061
 *
3062
 * @param object   $config    Tool proxy definition
3063
 *
3064
 * @return int  Record id number
3065
 */
3066
function lti_add_tool_proxy($config) {
3067
    global $USER, $DB;
3068
 
3069
    $toolproxy = new \stdClass();
3070
    if (isset($config->lti_registrationname)) {
3071
        $toolproxy->name = trim($config->lti_registrationname);
3072
    }
3073
    if (isset($config->lti_registrationurl)) {
3074
        $toolproxy->regurl = trim($config->lti_registrationurl);
3075
    }
3076
    if (isset($config->lti_capabilities)) {
3077
        $toolproxy->capabilityoffered = implode("\n", $config->lti_capabilities);
3078
    } else {
3079
        $toolproxy->capabilityoffered = implode("\n", array_keys(lti_get_capabilities()));
3080
    }
3081
    if (isset($config->lti_services)) {
3082
        $toolproxy->serviceoffered = implode("\n", $config->lti_services);
3083
    } else {
3084
        $func = function($s) {
3085
            return $s->get_id();
3086
        };
3087
        $servicenames = array_map($func, lti_get_services());
3088
        $toolproxy->serviceoffered = implode("\n", $servicenames);
3089
    }
3090
    if (isset($config->toolproxyid) && !empty($config->toolproxyid)) {
3091
        $toolproxy->id = $config->toolproxyid;
3092
        if (!isset($toolproxy->state) || ($toolproxy->state != LTI_TOOL_PROXY_STATE_ACCEPTED)) {
3093
            $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED;
3094
            $toolproxy->guid = random_string();
3095
            $toolproxy->secret = random_string();
3096
        }
3097
        $id = lti_update_tool_proxy($toolproxy);
3098
    } else {
3099
        $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED;
3100
        $toolproxy->timemodified = time();
3101
        $toolproxy->timecreated = $toolproxy->timemodified;
3102
        if (!isset($toolproxy->createdby)) {
3103
            $toolproxy->createdby = $USER->id;
3104
        }
3105
        $toolproxy->guid = random_string();
3106
        $toolproxy->secret = random_string();
3107
        $id = $DB->insert_record('lti_tool_proxies', $toolproxy);
3108
    }
3109
 
3110
    return $id;
3111
}
3112
 
3113
/**
3114
 * Updates a tool proxy in the database
3115
 *
3116
 * @param object  $toolproxy   Tool proxy
3117
 *
3118
 * @return int    Record id number
3119
 */
3120
function lti_update_tool_proxy($toolproxy) {
3121
    global $DB;
3122
 
3123
    $toolproxy->timemodified = time();
3124
    $id = $DB->update_record('lti_tool_proxies', $toolproxy);
3125
 
3126
    return $id;
3127
}
3128
 
3129
/**
3130
 * Delete a Tool Proxy
3131
 *
3132
 * @param int $id   Tool Proxy id
3133
 */
3134
function lti_delete_tool_proxy($id) {
3135
    global $DB;
3136
    $DB->delete_records('lti_tool_settings', array('toolproxyid' => $id));
3137
    $tools = $DB->get_records('lti_types', array('toolproxyid' => $id));
3138
    foreach ($tools as $tool) {
3139
        lti_delete_type($tool->id);
3140
    }
3141
    $DB->delete_records('lti_tool_proxies', array('id' => $id));
3142
}
3143
 
3144
/**
3145
 * Get both LTI tool proxies and tool types.
3146
 *
3147
 * If limit and offset are not zero, a subset of the tools will be returned. Tool proxies will be counted before tool
3148
 * types.
3149
 * For example: If 10 tool proxies and 10 tool types exist, and the limit is set to 15, then 10 proxies and 5 types
3150
 * will be returned.
3151
 *
3152
 * @param int $limit Maximum number of tools returned.
3153
 * @param int $offset Do not return tools before offset index.
3154
 * @param bool $orphanedonly If true, only return orphaned proxies.
3155
 * @param int $toolproxyid If not 0, only return tool types that have this tool proxy id.
3156
 * @return array list(proxies[], types[]) List containing array of tool proxies and array of tool types.
3157
 */
3158
function lti_get_lti_types_and_proxies(int $limit = 0, int $offset = 0, bool $orphanedonly = false, int $toolproxyid = 0): array {
3159
    global $DB;
3160
 
3161
    if ($orphanedonly) {
3162
        $orphanedproxiessql = helper::get_tool_proxy_sql($orphanedonly, false);
3163
        $countsql = helper::get_tool_proxy_sql($orphanedonly, true);
3164
        $proxies  = $DB->get_records_sql($orphanedproxiessql, null, $offset, $limit);
3165
        $totalproxiescount = $DB->count_records_sql($countsql);
3166
    } else {
3167
        $proxies = $DB->get_records('lti_tool_proxies', null, 'name ASC, state DESC, timemodified DESC',
3168
            '*', $offset, $limit);
3169
        $totalproxiescount = $DB->count_records('lti_tool_proxies');
3170
    }
3171
 
3172
    // Find new offset and limit for tool types after getting proxies and set up query.
3173
    $typesoffset = max($offset - $totalproxiescount, 0); // Set to 0 if negative.
3174
    $typeslimit = max($limit - count($proxies), 0); // Set to 0 if negative.
3175
    $typesparams = [];
3176
    if (!empty($toolproxyid)) {
3177
        $typesparams['toolproxyid'] = $toolproxyid;
3178
    }
3179
 
3180
    $types = $DB->get_records('lti_types', $typesparams, 'name ASC, state DESC, timemodified DESC',
3181
            '*', $typesoffset, $typeslimit);
3182
 
3183
    return [$proxies, array_map('serialise_tool_type', $types)];
3184
}
3185
 
3186
/**
3187
 * Get the total number of LTI tool types and tool proxies.
3188
 *
3189
 * @param bool $orphanedonly If true, only count orphaned proxies.
3190
 * @param int $toolproxyid If not 0, only count tool types that have this tool proxy id.
3191
 * @return int Count of tools.
3192
 */
3193
function lti_get_lti_types_and_proxies_count(bool $orphanedonly = false, int $toolproxyid = 0): int {
3194
    global $DB;
3195
 
3196
    $typessql = "SELECT count(*)
3197
                   FROM {lti_types}";
3198
    $typesparams = [];
3199
    if (!empty($toolproxyid)) {
3200
        $typessql .= " WHERE toolproxyid = :toolproxyid";
3201
        $typesparams['toolproxyid'] = $toolproxyid;
3202
    }
3203
 
3204
    $proxiessql = helper::get_tool_proxy_sql($orphanedonly, true);
3205
 
3206
    $countsql = "SELECT ($typessql) + ($proxiessql) as total" . $DB->sql_null_from_clause();
3207
 
3208
    return $DB->count_records_sql($countsql, $typesparams);
3209
}
3210
 
3211
/**
3212
 * Add a tool configuration in the database
3213
 *
3214
 * @param object $config   Tool configuration
3215
 *
3216
 * @return int Record id number
3217
 */
3218
function lti_add_config($config) {
3219
    global $DB;
3220
 
3221
    return $DB->insert_record('lti_types_config', $config);
3222
}
3223
 
3224
/**
3225
 * Updates a tool configuration in the database
3226
 *
3227
 * @param object  $config   Tool configuration
3228
 *
3229
 * @return mixed Record id number
3230
 */
3231
function lti_update_config($config) {
3232
    global $DB;
3233
 
3234
    $old = $DB->get_record('lti_types_config', array('typeid' => $config->typeid, 'name' => $config->name));
3235
 
3236
    if ($old) {
3237
        $config->id = $old->id;
3238
        $return = $DB->update_record('lti_types_config', $config);
3239
    } else {
3240
        $return = $DB->insert_record('lti_types_config', $config);
3241
    }
3242
    return $return;
3243
}
3244
 
3245
/**
3246
 * Gets the tool settings
3247
 *
3248
 * @param int  $toolproxyid   Id of tool proxy record (or tool ID if negative)
3249
 * @param int  $courseid      Id of course (null if system settings)
3250
 * @param int  $instanceid    Id of course module (null if system or context settings)
3251
 *
3252
 * @return array  Array settings
3253
 */
3254
function lti_get_tool_settings($toolproxyid, $courseid = null, $instanceid = null) {
3255
    global $DB;
3256
 
3257
    $settings = array();
3258
    if ($toolproxyid > 0) {
3259
        $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('toolproxyid' => $toolproxyid,
3260
            'course' => $courseid, 'coursemoduleid' => $instanceid));
3261
    } else {
3262
        $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('typeid' => -$toolproxyid,
3263
            'course' => $courseid, 'coursemoduleid' => $instanceid));
3264
    }
3265
    if ($settingsstr !== false) {
3266
        $settings = json_decode($settingsstr, true);
3267
    }
3268
    return $settings;
3269
}
3270
 
3271
/**
3272
 * Sets the tool settings (
3273
 *
3274
 * @param array  $settings      Array of settings
3275
 * @param int    $toolproxyid   Id of tool proxy record (or tool ID if negative)
3276
 * @param int    $courseid      Id of course (null if system settings)
3277
 * @param int    $instanceid    Id of course module (null if system or context settings)
3278
 */
3279
function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $instanceid = null) {
3280
    global $DB;
3281
 
3282
    $json = json_encode($settings);
3283
    if ($toolproxyid >= 0) {
3284
        $record = $DB->get_record('lti_tool_settings', array('toolproxyid' => $toolproxyid,
3285
            'course' => $courseid, 'coursemoduleid' => $instanceid));
3286
    } else {
3287
        $record = $DB->get_record('lti_tool_settings', array('typeid' => -$toolproxyid,
3288
            'course' => $courseid, 'coursemoduleid' => $instanceid));
3289
    }
3290
    if ($record !== false) {
3291
        $DB->update_record('lti_tool_settings', (object)array('id' => $record->id, 'settings' => $json, 'timemodified' => time()));
3292
    } else {
3293
        $record = new \stdClass();
3294
        if ($toolproxyid > 0) {
3295
            $record->toolproxyid = $toolproxyid;
3296
        } else {
3297
            $record->typeid = -$toolproxyid;
3298
        }
3299
        $record->course = $courseid;
3300
        $record->coursemoduleid = $instanceid;
3301
        $record->settings = $json;
3302
        $record->timecreated = time();
3303
        $record->timemodified = $record->timecreated;
3304
        $DB->insert_record('lti_tool_settings', $record);
3305
    }
3306
}
3307
 
3308
/**
3309
 * Signs the petition to launch the external tool using OAuth
3310
 *
3311
 * @param array  $oldparms     Parameters to be passed for signing
3312
 * @param string $endpoint     url of the external tool
3313
 * @param string $method       Method for sending the parameters (e.g. POST)
3314
 * @param string $oauthconsumerkey
3315
 * @param string $oauthconsumersecret
3316
 * @return array|null
3317
 */
3318
function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret) {
3319
 
3320
    $parms = $oldparms;
3321
 
3322
    $testtoken = '';
3323
 
3324
    // TODO: Switch to core oauthlib once implemented - MDL-30149.
3325
    $hmacmethod = new lti\OAuthSignatureMethod_HMAC_SHA1();
3326
    $testconsumer = new lti\OAuthConsumer($oauthconsumerkey, $oauthconsumersecret, null);
3327
    $accreq = lti\OAuthRequest::from_consumer_and_token($testconsumer, $testtoken, $method, $endpoint, $parms);
3328
    $accreq->sign_request($hmacmethod, $testconsumer, $testtoken);
3329
 
3330
    $newparms = $accreq->get_parameters();
3331
 
3332
    return $newparms;
3333
}
3334
 
3335
/**
3336
 * Converts the message paramters to their equivalent JWT claim and signs the payload to launch the external tool using JWT
3337
 *
3338
 * @param array  $parms        Parameters to be passed for signing
3339
 * @param string $endpoint     url of the external tool
3340
 * @param string $oauthconsumerkey
3341
 * @param string $typeid       ID of LTI tool type
3342
 * @param string $nonce        Nonce value to use
3343
 * @return array|null
3344
 */
3345
function lti_sign_jwt($parms, $endpoint, $oauthconsumerkey, $typeid = 0, $nonce = '') {
3346
    global $CFG;
3347
 
3348
    if (empty($typeid)) {
3349
        $typeid = 0;
3350
    }
3351
    $messagetypemapping = lti_get_jwt_message_type_mapping();
3352
    if (isset($parms['lti_message_type']) && array_key_exists($parms['lti_message_type'], $messagetypemapping)) {
3353
        $parms['lti_message_type'] = $messagetypemapping[$parms['lti_message_type']];
3354
    }
3355
    if (isset($parms['roles'])) {
3356
        $roles = explode(',', $parms['roles']);
3357
        $newroles = array();
3358
        foreach ($roles as $role) {
3359
            if (strpos($role, 'urn:lti:role:ims/lis/') === 0) {
3360
                $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#' . substr($role, 21);
3361
            } else if (strpos($role, 'urn:lti:instrole:ims/lis/') === 0) {
3362
                $role = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#' . substr($role, 25);
3363
            } else if (strpos($role, 'urn:lti:sysrole:ims/lis/') === 0) {
3364
                $role = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#' . substr($role, 24);
3365
            } else if ((strpos($role, '://') === false) && (strpos($role, 'urn:') !== 0)) {
3366
                $role = "http://purl.imsglobal.org/vocab/lis/v2/membership#{$role}";
3367
            }
3368
            $newroles[] = $role;
3369
        }
3370
        $parms['roles'] = implode(',', $newroles);
3371
    }
3372
 
3373
    $now = time();
3374
    if (empty($nonce)) {
3375
        $nonce = bin2hex(openssl_random_pseudo_bytes(10));
3376
    }
3377
    $claimmapping = lti_get_jwt_claim_mapping();
3378
    $payload = array(
3379
        'nonce' => $nonce,
3380
        'iat' => $now,
3381
        'exp' => $now + 60,
3382
    );
3383
    $payload['iss'] = $CFG->wwwroot;
3384
    $payload['aud'] = $oauthconsumerkey;
3385
    $payload[LTI_JWT_CLAIM_PREFIX . '/claim/deployment_id'] = strval($typeid);
3386
    $payload[LTI_JWT_CLAIM_PREFIX . '/claim/target_link_uri'] = $endpoint;
3387
 
3388
    foreach ($parms as $key => $value) {
3389
        $claim = LTI_JWT_CLAIM_PREFIX;
3390
        if (array_key_exists($key, $claimmapping)) {
3391
            $mapping = $claimmapping[$key];
3392
            $type = $mapping["type"] ?? "string";
3393
            if ($mapping['isarray']) {
3394
                $value = explode(',', $value);
3395
                sort($value);
3396
            } else if ($type == 'boolean') {
3397
                $value = isset($value) && ($value == 'true');
3398
            }
3399
            if (!empty($mapping['suffix'])) {
3400
                $claim .= "-{$mapping['suffix']}";
3401
            }
3402
            $claim .= '/claim/';
3403
            if (is_null($mapping['group'])) {
3404
                $payload[$mapping['claim']] = $value;
3405
            } else if (empty($mapping['group'])) {
3406
                $payload["{$claim}{$mapping['claim']}"] = $value;
3407
            } else {
3408
                $claim .= $mapping['group'];
3409
                $payload[$claim][$mapping['claim']] = $value;
3410
            }
3411
        } else if (strpos($key, 'custom_') === 0) {
3412
            $payload["{$claim}/claim/custom"][substr($key, 7)] = $value;
3413
        } else if (strpos($key, 'ext_') === 0) {
3414
            $payload["{$claim}/claim/ext"][substr($key, 4)] = $value;
3415
        }
3416
    }
3417
 
3418
    $privatekey = jwks_helper::get_private_key();
3419
    $jwt = JWT::encode($payload, $privatekey['key'], 'RS256', $privatekey['kid']);
3420
 
3421
    $newparms = array();
3422
    $newparms['id_token'] = $jwt;
3423
 
3424
    return $newparms;
3425
}
3426
 
3427
/**
3428
 * Verfies the JWT and converts its claims to their equivalent message parameter.
3429
 *
3430
 * @param int    $typeid
3431
 * @param string $jwtparam   JWT parameter
3432
 *
3433
 * @return array  message parameters
3434
 * @throws moodle_exception
3435
 */
3436
function lti_convert_from_jwt($typeid, $jwtparam) {
3437
 
3438
    $params = array();
3439
    $parts = explode('.', $jwtparam);
3440
    $ok = (count($parts) === 3);
3441
    if ($ok) {
3442
        $payload = JWT::urlsafeB64Decode($parts[1]);
3443
        $claims = json_decode($payload, true);
3444
        $ok = !is_null($claims) && !empty($claims['iss']);
3445
    }
3446
    if ($ok) {
3447
        lti_verify_jwt_signature($typeid, $claims['iss'], $jwtparam);
3448
        $params['oauth_consumer_key'] = $claims['iss'];
3449
        foreach (lti_get_jwt_claim_mapping() as $key => $mapping) {
3450
            $claim = LTI_JWT_CLAIM_PREFIX;
3451
            if (!empty($mapping['suffix'])) {
3452
                $claim .= "-{$mapping['suffix']}";
3453
            }
3454
            $claim .= '/claim/';
3455
            if (is_null($mapping['group'])) {
3456
                $claim = $mapping['claim'];
3457
            } else if (empty($mapping['group'])) {
3458
                $claim .= $mapping['claim'];
3459
            } else {
3460
                $claim .= $mapping['group'];
3461
            }
3462
            if (isset($claims[$claim])) {
3463
                $value = null;
3464
                if (empty($mapping['group'])) {
3465
                    $value = $claims[$claim];
3466
                } else {
3467
                    $group = $claims[$claim];
3468
                    if (is_array($group) && array_key_exists($mapping['claim'], $group)) {
3469
                        $value = $group[$mapping['claim']];
3470
                    }
3471
                }
3472
                if (!empty($value) && $mapping['isarray']) {
3473
                    if (is_array($value)) {
3474
                        if (is_array($value[0])) {
3475
                            $value = json_encode($value);
3476
                        } else {
3477
                            $value = implode(',', $value);
3478
                        }
3479
                    }
3480
                }
3481
                if (!is_null($value) && is_string($value) && (strlen($value) > 0)) {
3482
                    $params[$key] = $value;
3483
                }
3484
            }
3485
            $claim = LTI_JWT_CLAIM_PREFIX . '/claim/custom';
3486
            if (isset($claims[$claim])) {
3487
                $custom = $claims[$claim];
3488
                if (is_array($custom)) {
3489
                    foreach ($custom as $key => $value) {
3490
                        $params["custom_{$key}"] = $value;
3491
                    }
3492
                }
3493
            }
3494
            $claim = LTI_JWT_CLAIM_PREFIX . '/claim/ext';
3495
            if (isset($claims[$claim])) {
3496
                $ext = $claims[$claim];
3497
                if (is_array($ext)) {
3498
                    foreach ($ext as $key => $value) {
3499
                        $params["ext_{$key}"] = $value;
3500
                    }
3501
                }
3502
            }
3503
        }
3504
    }
3505
    if (isset($params['content_items'])) {
3506
        $params['content_items'] = lti_convert_content_items($params['content_items']);
3507
    }
3508
    $messagetypemapping = lti_get_jwt_message_type_mapping();
3509
    if (isset($params['lti_message_type']) && array_key_exists($params['lti_message_type'], $messagetypemapping)) {
3510
        $params['lti_message_type'] = $messagetypemapping[$params['lti_message_type']];
3511
    }
3512
    return $params;
3513
}
3514
 
3515
/**
3516
 * Posts the launch petition HTML
3517
 *
3518
 * @param array $newparms   Signed parameters
3519
 * @param string $endpoint  URL of the external tool
3520
 * @param bool $debug       Debug (true/false)
3521
 * @return string
3522
 */
3523
function lti_post_launch_html($newparms, $endpoint, $debug=false) {
3524
    $r = "<form action=\"" . $endpoint .
3525
        "\" name=\"ltiLaunchForm\" id=\"ltiLaunchForm\" method=\"post\" encType=\"application/x-www-form-urlencoded\">\n";
3526
 
3527
    // Contruct html for the launch parameters.
3528
    foreach ($newparms as $key => $value) {
3529
        $key = htmlspecialchars($key, ENT_COMPAT);
3530
        $value = htmlspecialchars($value, ENT_COMPAT);
3531
        if ( $key == "ext_submit" ) {
3532
            $r .= "<input type=\"submit\"";
3533
        } else {
3534
            $r .= "<input type=\"hidden\" name=\"{$key}\"";
3535
        }
3536
        $r .= " value=\"";
3537
        $r .= $value;
3538
        $r .= "\"/>\n";
3539
    }
3540
 
3541
    if ( $debug ) {
3542
        $r .= "<script language=\"javascript\"> \n";
3543
        $r .= "  //<![CDATA[ \n";
3544
        $r .= "function basicltiDebugToggle() {\n";
3545
        $r .= "    var ele = document.getElementById(\"basicltiDebug\");\n";
3546
        $r .= "    if (ele.style.display == \"block\") {\n";
3547
        $r .= "        ele.style.display = \"none\";\n";
3548
        $r .= "    }\n";
3549
        $r .= "    else {\n";
3550
        $r .= "        ele.style.display = \"block\";\n";
3551
        $r .= "    }\n";
3552
        $r .= "} \n";
3553
        $r .= "  //]]> \n";
3554
        $r .= "</script>\n";
3555
        $r .= "<a id=\"displayText\" href=\"javascript:basicltiDebugToggle();\">";
3556
        $r .= get_string("toggle_debug_data", "lti")."</a>\n";
3557
        $r .= "<div id=\"basicltiDebug\" style=\"display:none\">\n";
3558
        $r .= "<b>".get_string("basiclti_endpoint", "lti")."</b><br/>\n";
3559
        $r .= $endpoint . "<br/>\n&nbsp;<br/>\n";
3560
        $r .= "<b>".get_string("basiclti_parameters", "lti")."</b><br/>\n";
3561
        foreach ($newparms as $key => $value) {
3562
            $key = htmlspecialchars($key, ENT_COMPAT);
3563
            $value = htmlspecialchars($value, ENT_COMPAT);
3564
            $r .= "$key = $value<br/>\n";
3565
        }
3566
        $r .= "&nbsp;<br/>\n";
3567
        $r .= "</div>\n";
3568
    }
3569
    $r .= "</form>\n";
3570
 
3571
    // Auto-submit the form if endpoint is set.
3572
    if ($endpoint !== '' && !$debug) {
3573
        $r .= " <script type=\"text/javascript\"> \n" .
3574
            "  //<![CDATA[ \n" .
3575
            "    document.ltiLaunchForm.submit(); \n" .
3576
            "  //]]> \n" .
3577
            " </script> \n";
3578
    }
3579
    return $r;
3580
}
3581
 
3582
/**
3583
 * Generate the form for initiating a login request for an LTI 1.3 message
3584
 *
3585
 * @param int            $courseid  Course ID
3586
 * @param int            $cmid        LTI instance ID
3587
 * @param stdClass|null  $instance  LTI instance
3588
 * @param stdClass       $config    Tool type configuration
3589
 * @param string         $messagetype   LTI message type
3590
 * @param string         $title     Title of content item
3591
 * @param string         $text      Description of content item
3592
 * @param int            $foruserid Id of the user targeted by the launch
3593
 * @return string
3594
 */
3595
function lti_initiate_login($courseid, $cmid, $instance, $config, $messagetype = 'basic-lti-launch-request',
3596
        $title = '', $text = '', $foruserid = 0) {
3597
    global $SESSION;
3598
 
3599
    $params = lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid, $title, $text);
3600
 
3601
    $r = "<form action=\"" . $config->lti_initiatelogin .
3602
        "\" name=\"ltiInitiateLoginForm\" id=\"ltiInitiateLoginForm\" method=\"post\" " .
3603
        "encType=\"application/x-www-form-urlencoded\">\n";
3604
 
3605
    foreach ($params as $key => $value) {
3606
        $key = htmlspecialchars($key, ENT_COMPAT);
3607
        $value = htmlspecialchars($value, ENT_COMPAT);
3608
        $r .= "  <input type=\"hidden\" name=\"{$key}\" value=\"{$value}\"/>\n";
3609
    }
3610
    $r .= "</form>\n";
3611
 
3612
    $r .= "<script type=\"text/javascript\">\n" .
3613
        "//<![CDATA[\n" .
3614
        "document.ltiInitiateLoginForm.submit();\n" .
3615
        "//]]>\n" .
3616
        "</script>\n";
3617
 
3618
    return $r;
3619
}
3620
 
3621
/**
3622
 * Prepares an LTI 1.3 login request
3623
 *
3624
 * @param int            $courseid  Course ID
3625
 * @param int            $cmid        Course Module instance ID
3626
 * @param stdClass|null  $instance  LTI instance
3627
 * @param stdClass       $config    Tool type configuration
3628
 * @param string         $messagetype   LTI message type
3629
 * @param int            $foruserid Id of the user targeted by the launch
3630
 * @param string         $title     Title of content item
3631
 * @param string         $text      Description of content item
3632
 * @return array Login request parameters
3633
 */
3634
function lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid=0, $title = '', $text = '') {
3635
    global $USER, $CFG, $SESSION;
3636
    $ltihint = [];
3637
    if (!empty($instance)) {
3638
        $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $config->lti_toolurl;
3639
        $launchid = 'ltilaunch'.$instance->id.'_'.rand();
3640
        $ltihint['cmid'] = $cmid;
3641
        $SESSION->$launchid = "{$courseid},{$config->typeid},{$cmid},{$messagetype},{$foruserid},,";
3642
    } else {
3643
        $endpoint = $config->lti_toolurl;
3644
        if (($messagetype === 'ContentItemSelectionRequest') && !empty($config->lti_toolurl_ContentItemSelectionRequest)) {
3645
            $endpoint = $config->lti_toolurl_ContentItemSelectionRequest;
3646
        }
3647
        $launchid = "ltilaunch_$messagetype".rand();
3648
        $SESSION->$launchid =
3649
            "{$courseid},{$config->typeid},,{$messagetype},{$foruserid}," . base64_encode($title) . ',' . base64_encode($text);
3650
    }
3651
    $endpoint = trim($endpoint);
3652
    $services = lti_get_services();
3653
    foreach ($services as $service) {
3654
        [$endpoint] = $service->override_endpoint($messagetype ?? 'basic-lti-launch-request', $endpoint, '', $courseid, $instance);
3655
    }
3656
 
3657
    $ltihint['launchid'] = $launchid;
3658
    // If SSL is forced make sure https is on the normal launch URL.
3659
    if (isset($config->lti_forcessl) && ($config->lti_forcessl == '1')) {
3660
        $endpoint = lti_ensure_url_is_https($endpoint);
3661
    } else if (!strstr($endpoint, '://')) {
3662
        $endpoint = 'http://' . $endpoint;
3663
    }
3664
 
3665
    $params = array();
3666
    $params['iss'] = $CFG->wwwroot;
3667
    $params['target_link_uri'] = $endpoint;
3668
    $params['login_hint'] = $USER->id;
3669
    $params['lti_message_hint'] = json_encode($ltihint);
3670
    $params['client_id'] = $config->lti_clientid;
3671
    $params['lti_deployment_id'] = $config->typeid;
3672
    return $params;
3673
}
3674
 
3675
function lti_get_type($typeid) {
3676
    global $DB;
3677
 
3678
    return $DB->get_record('lti_types', array('id' => $typeid));
3679
}
3680
 
3681
function lti_get_launch_container($lti, $toolconfig) {
3682
    if (empty($lti->launchcontainer)) {
3683
        $lti->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
3684
    }
3685
 
3686
    if ($lti->launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) {
3687
        if (isset($toolconfig['launchcontainer'])) {
3688
            $launchcontainer = $toolconfig['launchcontainer'];
3689
        }
3690
    } else {
3691
        $launchcontainer = $lti->launchcontainer;
3692
    }
3693
 
3694
    if (empty($launchcontainer) || $launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) {
3695
        $launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
3696
    }
3697
 
3698
    $devicetype = core_useragent::get_device_type();
3699
 
3700
    // Scrolling within the object element doesn't work on iOS or Android
3701
    // Opening the popup window also had some issues in testing
3702
    // For mobile devices, always take up the entire screen to ensure the best experience.
3703
    if ($devicetype === core_useragent::DEVICETYPE_MOBILE || $devicetype === core_useragent::DEVICETYPE_TABLET ) {
3704
        $launchcontainer = LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW;
3705
    }
3706
 
3707
    return $launchcontainer;
3708
}
3709
 
3710
function lti_request_is_using_ssl() {
3711
    global $CFG;
3712
    return (stripos($CFG->wwwroot, 'https://') === 0);
3713
}
3714
 
3715
function lti_ensure_url_is_https($url) {
3716
    if (!strstr($url, '://')) {
3717
        $url = 'https://' . $url;
3718
    } else {
3719
        // If the URL starts with http, replace with https.
3720
        if (stripos($url, 'http://') === 0) {
3721
            $url = 'https://' . substr($url, 7);
3722
        }
3723
    }
3724
 
3725
    return $url;
3726
}
3727
 
3728
/**
3729
 * Determines if we should try to log the request
3730
 *
3731
 * @param string $rawbody
3732
 * @return bool
3733
 */
3734
function lti_should_log_request($rawbody) {
3735
    global $CFG;
3736
 
3737
    if (empty($CFG->mod_lti_log_users)) {
3738
        return false;
3739
    }
3740
 
3741
    $logusers = explode(',', $CFG->mod_lti_log_users);
3742
    if (empty($logusers)) {
3743
        return false;
3744
    }
3745
 
3746
    try {
3747
        $xml = new \SimpleXMLElement($rawbody);
3748
        $ns  = $xml->getNamespaces();
3749
        $ns  = array_shift($ns);
3750
        $xml->registerXPathNamespace('lti', $ns);
3751
        $requestuserid = '';
3752
        if ($node = $xml->xpath('//lti:userId')) {
3753
            $node = $node[0];
3754
            $requestuserid = clean_param((string) $node, PARAM_INT);
3755
        } else if ($node = $xml->xpath('//lti:sourcedId')) {
3756
            $node = $node[0];
3757
            $resultjson = json_decode((string) $node);
3758
            $requestuserid = clean_param($resultjson->data->userid, PARAM_INT);
3759
        }
3760
    } catch (Exception $e) {
3761
        return false;
3762
    }
3763
 
3764
    if (empty($requestuserid) or !in_array($requestuserid, $logusers)) {
3765
        return false;
3766
    }
3767
 
3768
    return true;
3769
}
3770
 
3771
/**
3772
 * Logs the request to a file in temp dir.
3773
 *
3774
 * @param string $rawbody
3775
 */
3776
function lti_log_request($rawbody) {
3777
    if ($tempdir = make_temp_directory('mod_lti', false)) {
3778
        if ($tempfile = tempnam($tempdir, 'mod_lti_request'.date('YmdHis'))) {
3779
            $content  = "Request Headers:\n";
3780
            foreach (moodle\mod\lti\OAuthUtil::get_headers() as $header => $value) {
3781
                $content .= "$header: $value\n";
3782
            }
3783
            $content .= "Request Body:\n";
3784
            $content .= $rawbody;
3785
 
3786
            file_put_contents($tempfile, $content);
3787
            chmod($tempfile, 0644);
3788
        }
3789
    }
3790
}
3791
 
3792
/**
3793
 * Log an LTI response.
3794
 *
3795
 * @param string $responsexml The response XML
3796
 * @param Exception $e If there was an exception, pass that too
3797
 */
3798
function lti_log_response($responsexml, $e = null) {
3799
    if ($tempdir = make_temp_directory('mod_lti', false)) {
3800
        if ($tempfile = tempnam($tempdir, 'mod_lti_response'.date('YmdHis'))) {
3801
            $content = '';
3802
            if ($e instanceof Exception) {
3803
                $info = get_exception_info($e);
3804
 
3805
                $content .= "Exception:\n";
3806
                $content .= "Message: $info->message\n";
3807
                $content .= "Debug info: $info->debuginfo\n";
3808
                $content .= "Backtrace:\n";
3809
                $content .= format_backtrace($info->backtrace, true);
3810
                $content .= "\n";
3811
            }
3812
            $content .= "Response XML:\n";
3813
            $content .= $responsexml;
3814
 
3815
            file_put_contents($tempfile, $content);
3816
            chmod($tempfile, 0644);
3817
        }
3818
    }
3819
}
3820
 
3821
/**
3822
 * Fetches LTI type configuration for an LTI instance
3823
 *
3824
 * @param stdClass $instance
3825
 * @return array Can be empty if no type is found
3826
 */
3827
function lti_get_type_config_by_instance($instance) {
3828
    $typeid = null;
3829
    if (empty($instance->typeid)) {
3830
        $tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course);
3831
        if ($tool) {
3832
            $typeid = $tool->id;
3833
        }
3834
    } else {
3835
        $typeid = $instance->typeid;
3836
    }
3837
    if (!empty($typeid)) {
3838
        return lti_get_type_config($typeid);
3839
    }
3840
    return array();
3841
}
3842
 
3843
/**
3844
 * Enforce type config settings onto the LTI instance
3845
 *
3846
 * @param stdClass $instance
3847
 * @param array $typeconfig
3848
 */
3849
function lti_force_type_config_settings($instance, array $typeconfig) {
3850
    $forced = array(
3851
        'instructorchoicesendname'      => 'sendname',
3852
        'instructorchoicesendemailaddr' => 'sendemailaddr',
3853
        'instructorchoiceacceptgrades'  => 'acceptgrades',
3854
    );
3855
 
3856
    foreach ($forced as $instanceparam => $typeconfigparam) {
3857
        if (array_key_exists($typeconfigparam, $typeconfig) && $typeconfig[$typeconfigparam] != LTI_SETTING_DELEGATE) {
3858
            $instance->$instanceparam = $typeconfig[$typeconfigparam];
3859
        }
3860
    }
3861
}
3862
 
3863
/**
3864
 * Initializes an array with the capabilities supported by the LTI module
3865
 *
3866
 * @return array List of capability names (without a dollar sign prefix)
3867
 */
3868
function lti_get_capabilities() {
3869
 
3870
    $capabilities = array(
3871
       'basic-lti-launch-request' => '',
3872
       'ContentItemSelectionRequest' => '',
3873
       'ToolProxyRegistrationRequest' => '',
3874
       'Context.id' => 'context_id',
3875
       'Context.title' => 'context_title',
3876
       'Context.label' => 'context_label',
3877
       'Context.id.history' => null,
3878
       'Context.sourcedId' => 'lis_course_section_sourcedid',
3879
       'Context.longDescription' => '$COURSE->summary',
3880
       'Context.timeFrame.begin' => '$COURSE->startdate',
3881
       'CourseSection.title' => 'context_title',
3882
       'CourseSection.label' => 'context_label',
3883
       'CourseSection.sourcedId' => 'lis_course_section_sourcedid',
3884
       'CourseSection.longDescription' => '$COURSE->summary',
3885
       'CourseSection.timeFrame.begin' => null,
3886
       'CourseSection.timeFrame.end' => null,
3887
       'ResourceLink.id' => 'resource_link_id',
3888
       'ResourceLink.title' => 'resource_link_title',
3889
       'ResourceLink.description' => 'resource_link_description',
3890
       'User.id' => 'user_id',
3891
       'User.username' => '$USER->username',
3892
       'Person.name.full' => 'lis_person_name_full',
3893
       'Person.name.given' => 'lis_person_name_given',
3894
       'Person.name.family' => 'lis_person_name_family',
3895
       'Person.email.primary' => 'lis_person_contact_email_primary',
3896
       'Person.sourcedId' => 'lis_person_sourcedid',
3897
       'Person.name.middle' => '$USER->middlename',
3898
       'Person.address.street1' => '$USER->address',
3899
       'Person.address.locality' => '$USER->city',
3900
       'Person.address.country' => '$USER->country',
3901
       'Person.address.timezone' => '$USER->timezone',
3902
       'Person.phone.primary' => '$USER->phone1',
3903
       'Person.phone.mobile' => '$USER->phone2',
3904
       'Person.webaddress' => '$USER->url',
3905
       'Membership.role' => 'roles',
3906
       'Result.sourcedId' => 'lis_result_sourcedid',
3907
       'Result.autocreate' => 'lis_outcome_service_url',
3908
       'BasicOutcome.sourcedId' => 'lis_result_sourcedid',
3909
       'BasicOutcome.url' => 'lis_outcome_service_url',
3910
       'Moodle.Person.userGroupIds' => null);
3911
 
3912
    return $capabilities;
3913
 
3914
}
3915
 
3916
/**
3917
 * Initializes an array with the services supported by the LTI module
3918
 *
3919
 * @return array List of services
3920
 */
3921
function lti_get_services() {
3922
 
3923
    $services = array();
3924
    $definedservices = core_component::get_plugin_list('ltiservice');
3925
    foreach ($definedservices as $name => $location) {
3926
        $classname = "\\ltiservice_{$name}\\local\\service\\{$name}";
3927
        $services[] = new $classname();
3928
    }
3929
 
3930
    return $services;
3931
 
3932
}
3933
 
3934
/**
3935
 * Initializes an instance of the named service
3936
 *
3937
 * @param string $servicename Name of service
3938
 *
3939
 * @return bool|\mod_lti\local\ltiservice\service_base Service
3940
 */
3941
function lti_get_service_by_name($servicename) {
3942
 
3943
    $service = false;
3944
    $classname = "\\ltiservice_{$servicename}\\local\\service\\{$servicename}";
3945
    if (class_exists($classname)) {
3946
        $service = new $classname();
3947
    }
3948
 
3949
    return $service;
3950
 
3951
}
3952
 
3953
/**
3954
 * Finds a service by id
3955
 *
3956
 * @param \mod_lti\local\ltiservice\service_base[] $services Array of services
3957
 * @param string $resourceid  ID of resource
3958
 *
3959
 * @return mod_lti\local\ltiservice\service_base Service
3960
 */
3961
function lti_get_service_by_resource_id($services, $resourceid) {
3962
 
3963
    $service = false;
3964
    foreach ($services as $aservice) {
3965
        foreach ($aservice->get_resources() as $resource) {
3966
            if ($resource->get_id() === $resourceid) {
3967
                $service = $aservice;
3968
                break 2;
3969
            }
3970
        }
3971
    }
3972
 
3973
    return $service;
3974
 
3975
}
3976
 
3977
/**
3978
 * Initializes an array with the scopes for services supported by the LTI module
3979
 * and authorized for this particular tool instance.
3980
 *
3981
 * @param object $type  LTI tool type
3982
 * @param array  $typeconfig  LTI tool type configuration
3983
 *
3984
 * @return array List of scopes
3985
 */
3986
function lti_get_permitted_service_scopes($type, $typeconfig) {
3987
 
3988
    $services = lti_get_services();
3989
    $scopes = array();
3990
    foreach ($services as $service) {
3991
        $service->set_type($type);
3992
        $service->set_typeconfig($typeconfig);
3993
        $servicescopes = $service->get_permitted_scopes();
3994
        if (!empty($servicescopes)) {
3995
            $scopes = array_merge($scopes, $servicescopes);
3996
        }
3997
    }
3998
 
3999
    return $scopes;
4000
}
4001
 
4002
/**
4003
 * Extracts the named contexts from a tool proxy
4004
 *
4005
 * @param object $json
4006
 *
4007
 * @return array Contexts
4008
 */
4009
function lti_get_contexts($json) {
4010
 
4011
    $contexts = array();
4012
    if (isset($json->{'@context'})) {
4013
        foreach ($json->{'@context'} as $context) {
4014
            if (is_object($context)) {
4015
                $contexts = array_merge(get_object_vars($context), $contexts);
4016
            }
4017
        }
4018
    }
4019
 
4020
    return $contexts;
4021
 
4022
}
4023
 
4024
/**
4025
 * Converts an ID to a fully-qualified ID
4026
 *
4027
 * @param array $contexts
4028
 * @param string $id
4029
 *
4030
 * @return string Fully-qualified ID
4031
 */
4032
function lti_get_fqid($contexts, $id) {
4033
 
4034
    $parts = explode(':', $id, 2);
4035
    if (count($parts) > 1) {
4036
        if (array_key_exists($parts[0], $contexts)) {
4037
            $id = $contexts[$parts[0]] . $parts[1];
4038
        }
4039
    }
4040
 
4041
    return $id;
4042
 
4043
}
4044
 
4045
/**
4046
 * Returns the icon for the given tool type
4047
 *
4048
 * @param stdClass $type The tool type
4049
 *
4050
 * @return string The url to the tool type's corresponding icon
4051
 */
4052
function get_tool_type_icon_url(stdClass $type) {
4053
    global $OUTPUT;
4054
 
4055
    $iconurl = $type->secureicon;
4056
 
4057
    if (empty($iconurl)) {
4058
        $iconurl = $type->icon;
4059
    }
4060
 
4061
    if (empty($iconurl)) {
4062
        $iconurl = $OUTPUT->image_url('monologo', 'lti')->out();
4063
    }
4064
 
4065
    return $iconurl;
4066
}
4067
 
4068
/**
4069
 * Returns the edit url for the given tool type
4070
 *
4071
 * @param stdClass $type The tool type
4072
 *
4073
 * @return string The url to edit the tool type
4074
 */
4075
function get_tool_type_edit_url(stdClass $type) {
4076
    $url = new moodle_url('/mod/lti/typessettings.php',
4077
                          array('action' => 'update', 'id' => $type->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure'));
4078
    return $url->out();
4079
}
4080
 
4081
/**
4082
 * Returns the edit url for the given tool proxy.
4083
 *
4084
 * @param stdClass $proxy The tool proxy
4085
 *
4086
 * @return string The url to edit the tool type
4087
 */
4088
function get_tool_proxy_edit_url(stdClass $proxy) {
4089
    $url = new moodle_url('/mod/lti/registersettings.php',
4090
                          array('action' => 'update', 'id' => $proxy->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure'));
4091
    return $url->out();
4092
}
4093
 
4094
/**
4095
 * Returns the course url for the given tool type
4096
 *
4097
 * @param stdClass $type The tool type
4098
 *
4099
 * @return string The url to the course of the tool type, void if it is a site wide type
4100
 */
4101
function get_tool_type_course_url(stdClass $type) {
4102
    if ($type->course != 1) {
4103
        $url = new moodle_url('/course/view.php', array('id' => $type->course));
4104
        return $url->out();
4105
    }
4106
    return null;
4107
}
4108
 
4109
/**
4110
 * Returns the icon and edit urls for the tool type and the course url if it is a course type.
4111
 *
4112
 * @param stdClass $type The tool type
4113
 *
4114
 * @return array The urls of the tool type
4115
 */
4116
function get_tool_type_urls(stdClass $type) {
4117
    $courseurl = get_tool_type_course_url($type);
4118
 
4119
    $urls = array(
4120
        'icon' => get_tool_type_icon_url($type),
4121
        'edit' => get_tool_type_edit_url($type),
4122
    );
4123
 
4124
    if ($courseurl) {
4125
        $urls['course'] = $courseurl;
4126
    }
4127
 
4128
    $url = new moodle_url('/mod/lti/certs.php');
4129
    $urls['publickeyset'] = $url->out();
4130
    $url = new moodle_url('/mod/lti/token.php');
4131
    $urls['accesstoken'] = $url->out();
4132
    $url = new moodle_url('/mod/lti/auth.php');
4133
    $urls['authrequest'] = $url->out();
4134
 
4135
    return $urls;
4136
}
4137
 
4138
/**
4139
 * Returns the icon and edit urls for the tool proxy.
4140
 *
4141
 * @param stdClass $proxy The tool proxy
4142
 *
4143
 * @return array The urls of the tool proxy
4144
 */
4145
function get_tool_proxy_urls(stdClass $proxy) {
4146
    global $OUTPUT;
4147
 
4148
    $urls = array(
4149
        'icon' => $OUTPUT->image_url('monologo', 'lti')->out(),
4150
        'edit' => get_tool_proxy_edit_url($proxy),
4151
    );
4152
 
4153
    return $urls;
4154
}
4155
 
4156
/**
4157
 * Returns information on the current state of the tool type
4158
 *
4159
 * @param stdClass $type The tool type
4160
 *
4161
 * @return array An array with a text description of the state, and boolean for whether it is in each state:
4162
 * pending, configured, rejected, unknown
4163
 */
4164
function get_tool_type_state_info(stdClass $type) {
4165
    $isconfigured = false;
4166
    $ispending = false;
4167
    $isrejected = false;
4168
    $isunknown = false;
4169
    switch ($type->state) {
4170
        case LTI_TOOL_STATE_CONFIGURED:
4171
            $state = get_string('active', 'mod_lti');
4172
            $isconfigured = true;
4173
            break;
4174
        case LTI_TOOL_STATE_PENDING:
4175
            $state = get_string('pending', 'mod_lti');
4176
            $ispending = true;
4177
            break;
4178
        case LTI_TOOL_STATE_REJECTED:
4179
            $state = get_string('rejected', 'mod_lti');
4180
            $isrejected = true;
4181
            break;
4182
        default:
4183
            $state = get_string('unknownstate', 'mod_lti');
4184
            $isunknown = true;
4185
            break;
4186
    }
4187
 
4188
    return array(
4189
        'text' => $state,
4190
        'pending' => $ispending,
4191
        'configured' => $isconfigured,
4192
        'rejected' => $isrejected,
4193
        'unknown' => $isunknown
4194
    );
4195
}
4196
 
4197
/**
4198
 * Returns information on the configuration of the tool type
4199
 *
4200
 * @param stdClass $type The tool type
4201
 *
4202
 * @return array An array with configuration details
4203
 */
4204
function get_tool_type_config($type) {
4205
    global $CFG;
4206
    $platformid = $CFG->wwwroot;
4207
    $clientid = $type->clientid;
4208
    $deploymentid = $type->id;
4209
    $publickeyseturl = new moodle_url('/mod/lti/certs.php');
4210
    $publickeyseturl = $publickeyseturl->out();
4211
 
4212
    $accesstokenurl = new moodle_url('/mod/lti/token.php');
4213
    $accesstokenurl = $accesstokenurl->out();
4214
 
4215
    $authrequesturl = new moodle_url('/mod/lti/auth.php');
4216
    $authrequesturl = $authrequesturl->out();
4217
 
4218
    return array(
4219
        'platformid' => $platformid,
4220
        'clientid' => $clientid,
4221
        'deploymentid' => $deploymentid,
4222
        'publickeyseturl' => $publickeyseturl,
4223
        'accesstokenurl' => $accesstokenurl,
4224
        'authrequesturl' => $authrequesturl
4225
    );
4226
}
4227
 
4228
/**
4229
 * Returns a summary of each LTI capability this tool type requires in plain language
4230
 *
4231
 * @param stdClass $type The tool type
4232
 *
4233
 * @return array An array of text descriptions of each of the capabilities this tool type requires
4234
 */
4235
function get_tool_type_capability_groups($type) {
4236
    $capabilities = lti_get_enabled_capabilities($type);
4237
    $groups = array();
4238
    $hascourse = false;
4239
    $hasactivities = false;
4240
    $hasuseraccount = false;
4241
    $hasuserpersonal = false;
4242
 
4243
    foreach ($capabilities as $capability) {
4244
        // Bail out early if we've already found all groups.
4245
        if (count($groups) >= 4) {
4246
            continue;
4247
        }
4248
 
4249
        if (!$hascourse && preg_match('/^CourseSection/', $capability)) {
4250
            $hascourse = true;
4251
            $groups[] = get_string('courseinformation', 'mod_lti');
4252
        } else if (!$hasactivities && preg_match('/^ResourceLink/', $capability)) {
4253
            $hasactivities = true;
4254
            $groups[] = get_string('courseactivitiesorresources', 'mod_lti');
4255
        } else if (!$hasuseraccount && preg_match('/^User/', $capability) || preg_match('/^Membership/', $capability)) {
4256
            $hasuseraccount = true;
4257
            $groups[] = get_string('useraccountinformation', 'mod_lti');
4258
        } else if (!$hasuserpersonal && preg_match('/^Person/', $capability)) {
4259
            $hasuserpersonal = true;
4260
            $groups[] = get_string('userpersonalinformation', 'mod_lti');
4261
        }
4262
    }
4263
 
4264
    return $groups;
4265
}
4266
 
4267
 
4268
/**
4269
 * Returns the ids of each instance of this tool type
4270
 *
4271
 * @param stdClass $type The tool type
4272
 *
4273
 * @return array An array of ids of the instances of this tool type
4274
 */
4275
function get_tool_type_instance_ids($type) {
4276
    global $DB;
4277
 
4278
    return array_keys($DB->get_fieldset_select('lti', 'id', 'typeid = ?', array($type->id)));
4279
}
4280
 
4281
/**
4282
 * Serialises this tool type
4283
 *
4284
 * @param stdClass $type The tool type
4285
 *
4286
 * @return array An array of values representing this type
4287
 */
4288
function serialise_tool_type(stdClass $type) {
4289
    global $CFG;
4290
 
4291
    $capabilitygroups = get_tool_type_capability_groups($type);
4292
    $instanceids = get_tool_type_instance_ids($type);
4293
    // Clean the name. We don't want tags here.
4294
    $name = clean_param($type->name, PARAM_NOTAGS);
4295
    if (!empty($type->description)) {
4296
        // Clean the description. We don't want tags here.
4297
        $description = clean_param($type->description, PARAM_NOTAGS);
4298
    } else {
4299
        $description = get_string('editdescription', 'mod_lti');
4300
    }
4301
    return array(
4302
        'id' => $type->id,
4303
        'name' => $name,
4304
        'description' => $description,
4305
        'urls' => get_tool_type_urls($type),
4306
        'state' => get_tool_type_state_info($type),
4307
        'platformid' => $CFG->wwwroot,
4308
        'clientid' => $type->clientid,
4309
        'deploymentid' => $type->id,
4310
        'hascapabilitygroups' => !empty($capabilitygroups),
4311
        'capabilitygroups' => $capabilitygroups,
4312
        // Course ID of 1 means it's not linked to a course.
4313
        'courseid' => $type->course == 1 ? 0 : $type->course,
4314
        'instanceids' => $instanceids,
4315
        'instancecount' => count($instanceids)
4316
    );
4317
}
4318
 
4319
/**
4320
 * Loads the cartridge information into the tool type, if the launch url is for a cartridge file
4321
 *
4322
 * @param stdClass $type The tool type object to be filled in
4323
 * @since Moodle 3.1
4324
 */
4325
function lti_load_type_if_cartridge($type) {
4326
    if (!empty($type->lti_toolurl) && lti_is_cartridge($type->lti_toolurl)) {
4327
        lti_load_type_from_cartridge($type->lti_toolurl, $type);
4328
    }
4329
}
4330
 
4331
/**
4332
 * Loads the cartridge information into the new tool, if the launch url is for a cartridge file
4333
 *
4334
 * @param stdClass $lti The tools config
4335
 * @since Moodle 3.1
4336
 */
4337
function lti_load_tool_if_cartridge($lti) {
4338
    if (!empty($lti->toolurl) && lti_is_cartridge($lti->toolurl)) {
4339
        lti_load_tool_from_cartridge($lti->toolurl, $lti);
4340
    }
4341
}
4342
 
4343
/**
4344
 * Determines if the given url is for a IMS basic cartridge
4345
 *
4346
 * @param  string $url The url to be checked
4347
 * @return True if the url is for a cartridge
4348
 * @since Moodle 3.1
4349
 */
4350
function lti_is_cartridge($url) {
4351
    // If it is empty, it's not a cartridge.
4352
    if (empty($url)) {
4353
        return false;
4354
    }
4355
    // If it has xml at the end of the url, it's a cartridge.
4356
    if (preg_match('/\.xml$/', $url)) {
4357
        return true;
4358
    }
1441 ariadna 4359
 
4360
    // Skip slow cartridge checks during tests.
4361
    // During tests, working .xml cartridge URLs are used when testing cartridge support. These will match the '.xml' check
4362
    // above (which is fast). Don't try to check whether other tool URLs are cartridges because most URLs used in tests will be
4363
    // example URLs and won't be resolvable, resulting in network hangs within load_cartridge() - which is called every time a
4364
    // tool is edited and will result in slow tests or seemingly random test failures.
4365
    if (!defined('BEHAT_SITE_RUNNING') && !defined('PHPUNIT_TEST')) {
4366
        // Even if it doesn't have .xml, load the url to check if it's a cartridge..
4367
        try {
4368
            $toolinfo = lti_load_cartridge($url,
4369
                array(
4370
                    "launch_url" => "launchurl"
4371
                )
4372
            );
4373
            if (!empty($toolinfo['launchurl'])) {
4374
                return true;
4375
            }
4376
        } catch (moodle_exception $e) {
4377
            return false; // Error loading the xml, so it's not a cartridge.
1 efrain 4378
        }
4379
    }
4380
    return false;
4381
}
4382
 
4383
/**
4384
 * Allows you to load settings for an external tool type from an IMS cartridge.
4385
 *
4386
 * @param  string   $url     The URL to the cartridge
4387
 * @param  stdClass $type    The tool type object to be filled in
4388
 * @throws moodle_exception if the cartridge could not be loaded correctly
4389
 * @since Moodle 3.1
4390
 */
4391
function lti_load_type_from_cartridge($url, $type) {
4392
    $toolinfo = lti_load_cartridge($url,
4393
        array(
4394
            "title" => "lti_typename",
4395
            "launch_url" => "lti_toolurl",
4396
            "description" => "lti_description",
4397
            "icon" => "lti_icon",
4398
            "secure_icon" => "lti_secureicon"
4399
        ),
4400
        array(
4401
            "icon_url" => "lti_extension_icon",
4402
            "secure_icon_url" => "lti_extension_secureicon"
4403
        )
4404
    );
4405
    // If an activity name exists, unset the cartridge name so we don't override it.
4406
    if (isset($type->lti_typename)) {
4407
        unset($toolinfo['lti_typename']);
4408
    }
4409
 
4410
    // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
4411
    if (empty($toolinfo['lti_icon']) && !empty($toolinfo['lti_extension_icon'])) {
4412
        $toolinfo['lti_icon'] = $toolinfo['lti_extension_icon'];
4413
    }
4414
    unset($toolinfo['lti_extension_icon']);
4415
 
4416
    if (empty($toolinfo['lti_secureicon']) && !empty($toolinfo['lti_extension_secureicon'])) {
4417
        $toolinfo['lti_secureicon'] = $toolinfo['lti_extension_secureicon'];
4418
    }
4419
    unset($toolinfo['lti_extension_secureicon']);
4420
 
4421
    // Ensure Custom icons aren't overridden by cartridge params.
4422
    if (!empty($type->lti_icon)) {
4423
        unset($toolinfo['lti_icon']);
4424
    }
4425
 
4426
    if (!empty($type->lti_secureicon)) {
4427
        unset($toolinfo['lti_secureicon']);
4428
    }
4429
 
4430
    foreach ($toolinfo as $property => $value) {
4431
        $type->$property = $value;
4432
    }
4433
}
4434
 
4435
/**
4436
 * Allows you to load in the configuration for an external tool from an IMS cartridge.
4437
 *
4438
 * @param  string   $url    The URL to the cartridge
4439
 * @param  stdClass $lti    LTI object
4440
 * @throws moodle_exception if the cartridge could not be loaded correctly
4441
 * @since Moodle 3.1
4442
 */
4443
function lti_load_tool_from_cartridge($url, $lti) {
4444
    $toolinfo = lti_load_cartridge($url,
4445
        array(
4446
            "title" => "name",
4447
            "launch_url" => "toolurl",
4448
            "secure_launch_url" => "securetoolurl",
4449
            "description" => "intro",
4450
            "icon" => "icon",
4451
            "secure_icon" => "secureicon"
4452
        ),
4453
        array(
4454
            "icon_url" => "extension_icon",
4455
            "secure_icon_url" => "extension_secureicon"
4456
        )
4457
    );
4458
    // If an activity name exists, unset the cartridge name so we don't override it.
4459
    if (isset($lti->name)) {
4460
        unset($toolinfo['name']);
4461
    }
4462
 
4463
    // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
4464
    if (empty($toolinfo['icon']) && !empty($toolinfo['extension_icon'])) {
4465
        $toolinfo['icon'] = $toolinfo['extension_icon'];
4466
    }
4467
    unset($toolinfo['extension_icon']);
4468
 
4469
    if (empty($toolinfo['secureicon']) && !empty($toolinfo['extension_secureicon'])) {
4470
        $toolinfo['secureicon'] = $toolinfo['extension_secureicon'];
4471
    }
4472
    unset($toolinfo['extension_secureicon']);
4473
 
4474
    foreach ($toolinfo as $property => $value) {
4475
        $lti->$property = $value;
4476
    }
4477
}
4478
 
4479
/**
4480
 * Search for a tag within an XML DOMDocument
4481
 *
4482
 * @param  string $url The url of the cartridge to be loaded
4483
 * @param  array  $map The map of tags to keys in the return array
4484
 * @param  array  $propertiesmap The map of properties to keys in the return array
4485
 * @return array An associative array with the given keys and their values from the cartridge
4486
 * @throws moodle_exception if the cartridge could not be loaded correctly
4487
 * @since Moodle 3.1
4488
 */
4489
function lti_load_cartridge($url, $map, $propertiesmap = array()) {
4490
    global $CFG;
4491
    require_once($CFG->libdir. "/filelib.php");
4492
 
4493
    $curl = new curl();
4494
    $response = $curl->get($url);
4495
 
4496
    // Got a completely empty response (real or error), cannot process this with
4497
    // DOMDocument::loadXML() because it errors with ValueError. So let's throw
4498
    // the moodle_exception before waiting to examine the errors later.
4499
    if (trim($response) === '') {
4500
        throw new moodle_exception('errorreadingfile', '', '', $url);
4501
    }
4502
 
4503
    // TODO MDL-46023 Replace this code with a call to the new library.
4504
    $origerrors = libxml_use_internal_errors(true);
4505
    libxml_clear_errors();
4506
 
4507
    $document = new DOMDocument();
4508
    @$document->loadXML($response, LIBXML_NONET);
4509
 
4510
    $cartridge = new DomXpath($document);
4511
 
4512
    $errors = libxml_get_errors();
4513
 
4514
    libxml_clear_errors();
4515
    libxml_use_internal_errors($origerrors);
4516
 
4517
    if (count($errors) > 0) {
4518
        $message = 'Failed to load cartridge.';
4519
        foreach ($errors as $error) {
4520
            $message .= "\n" . trim($error->message, "\n\r\t .") . " at line " . $error->line;
4521
        }
4522
        throw new moodle_exception('errorreadingfile', '', '', $url, $message);
4523
    }
4524
 
4525
    $toolinfo = array();
4526
    foreach ($map as $tag => $key) {
4527
        $value = get_tag($tag, $cartridge);
4528
        if ($value) {
4529
            $toolinfo[$key] = $value;
4530
        }
4531
    }
4532
    if (!empty($propertiesmap)) {
4533
        foreach ($propertiesmap as $property => $key) {
4534
            $value = get_tag("property", $cartridge, $property);
4535
            if ($value) {
4536
                $toolinfo[$key] = $value;
4537
            }
4538
        }
4539
    }
4540
 
4541
    return $toolinfo;
4542
}
4543
 
4544
/**
4545
 * Search for a tag within an XML DOMDocument
4546
 *
4547
 * @param  stdClass $tagname The name of the tag to search for
4548
 * @param  XPath    $xpath   The XML to find the tag in
4549
 * @param  XPath    $attribute The attribute to search for (if we should search for a child node with the given
4550
 * value for the name attribute
4551
 * @since Moodle 3.1
4552
 */
4553
function get_tag($tagname, $xpath, $attribute = null) {
4554
    if ($attribute) {
4555
        $result = $xpath->query('//*[local-name() = \'' . $tagname . '\'][@name="' . $attribute . '"]');
4556
    } else {
4557
        $result = $xpath->query('//*[local-name() = \'' . $tagname . '\']');
4558
    }
4559
    if ($result->length > 0) {
4560
        return $result->item(0)->nodeValue;
4561
    }
4562
    return null;
4563
}
4564
 
4565
/**
4566
 * Create a new access token.
4567
 *
4568
 * @param int $typeid Tool type ID
4569
 * @param string[] $scopes Scopes permitted for new token
4570
 *
4571
 * @return stdClass Access token
4572
 */
4573
function lti_new_access_token($typeid, $scopes) {
4574
    global $DB;
4575
 
4576
    // Make sure the token doesn't exist (even if it should be almost impossible with the random generation).
4577
    $numtries = 0;
4578
    do {
4579
        $numtries ++;
4580
        $generatedtoken = md5(uniqid(rand(), 1));
4581
        if ($numtries > 5) {
4582
            throw new moodle_exception('Failed to generate LTI access token');
4583
        }
4584
    } while ($DB->record_exists('lti_access_tokens', array('token' => $generatedtoken)));
4585
    $newtoken = new stdClass();
4586
    $newtoken->typeid = $typeid;
4587
    $newtoken->scope = json_encode(array_values($scopes));
4588
    $newtoken->token = $generatedtoken;
4589
 
4590
    $newtoken->timecreated = time();
4591
    $newtoken->validuntil = $newtoken->timecreated + LTI_ACCESS_TOKEN_LIFE;
4592
    $newtoken->lastaccess = null;
4593
 
4594
    $DB->insert_record('lti_access_tokens', $newtoken);
4595
 
4596
    return $newtoken;
4597
}