Proyectos de Subversion Moodle

Rev

Ir a la última revisión | | 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
        // JWK::parseKeySet uses RS256 algorithm by default.
1327
        $keys = JWK::parseKeySet($keysetarr);
1328
        $jwt = JWT::decode($jwtparam, $keys);
1329
    } catch (Exception $e) {
1330
        // Something went wrong, so attempt to update cached keyset and then try again.
1331
        $keyset = download_file_content($keyseturl);
1332
        $keysetarr = json_decode($keyset, true);
1333
 
1334
        // Fix for firebase/php-jwt's dependency on the optional 'alg' property in the JWK.
1335
        $keysetarr = jwks_helper::fix_jwks_alg($keysetarr, $jwtparam);
1336
 
1337
        // JWK::parseKeySet uses RS256 algorithm by default.
1338
        $keys = JWK::parseKeySet($keysetarr);
1339
        $jwt = JWT::decode($jwtparam, $keys);
1340
        // If sucessful, updates the cached keyset.
1341
        $cache->set($clientid, $keyset);
1342
    }
1343
    return $jwt;
1344
}
1345
 
1346
/**
1347
 * Verifies the JWT signature of an incoming message.
1348
 *
1349
 * @param int $typeid The tool type ID.
1350
 * @param string $consumerkey The consumer key.
1351
 * @param string $jwtparam JWT parameter value
1352
 *
1353
 * @return stdClass Tool type
1354
 * @throws moodle_exception
1355
 * @throws UnexpectedValueException     Provided JWT was invalid
1356
 * @throws SignatureInvalidException    Provided JWT was invalid because the signature verification failed
1357
 * @throws BeforeValidException         Provided JWT is trying to be used before it's eligible as defined by 'nbf'
1358
 * @throws BeforeValidException         Provided JWT is trying to be used before it's been created as defined by 'iat'
1359
 * @throws ExpiredException             Provided JWT has since expired, as defined by the 'exp' claim
1360
 */
1361
function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) {
1362
    $tool = lti_get_type($typeid);
1363
 
1364
    // Validate parameters.
1365
    if (!$tool) {
1366
        throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1367
    }
1368
    if (isset($tool->toolproxyid)) {
1369
        throw new moodle_exception('JWT security not supported with LTI 2');
1370
    }
1371
 
1372
    $typeconfig = lti_get_type_config($typeid);
1373
 
1374
    $key = $tool->clientid ?? '';
1375
 
1376
    if ($consumerkey !== $key) {
1377
        throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti');
1378
    }
1379
 
1380
    if (empty($typeconfig['keytype']) || $typeconfig['keytype'] === LTI_RSA_KEY) {
1381
        $publickey = $typeconfig['publickey'] ?? '';
1382
        if (empty($publickey)) {
1383
            throw new moodle_exception('No public key configured');
1384
        }
1385
        // Attemps to verify jwt with RSA key.
1386
        JWT::decode($jwtparam, new Key($publickey, 'RS256'));
1387
    } else if ($typeconfig['keytype'] === LTI_JWK_KEYSET) {
1388
        $keyseturl = $typeconfig['publickeyset'] ?? '';
1389
        if (empty($keyseturl)) {
1390
            throw new moodle_exception('No public keyset configured');
1391
        }
1392
        // Attempts to verify jwt with jwk keyset.
1393
        lti_verify_with_keyset($jwtparam, $keyseturl, $tool->clientid);
1394
    } else {
1395
        throw new moodle_exception('Invalid public key type');
1396
    }
1397
 
1398
    return $tool;
1399
}
1400
 
1401
/**
1402
 * Converts an array of custom parameters to a new line separated string.
1403
 *
1404
 * @param object $params list of params to concatenate
1405
 *
1406
 * @return string
1407
 */
1408
function params_to_string(object $params) {
1409
    $customparameters = [];
1410
    foreach ($params as $key => $value) {
1411
        $customparameters[] = "{$key}={$value}";
1412
    }
1413
    return implode("\n", $customparameters);
1414
}
1415
 
1416
/**
1417
 * Converts LTI 1.1 Content Item for LTI Link to Form data.
1418
 *
1419
 * @param object $tool Tool for which the item is created for.
1420
 * @param object $typeconfig The tool configuration.
1421
 * @param object $item Item populated from JSON to be converted to Form form
1422
 *
1423
 * @return stdClass Form config for the item
1424
 */
1425
function content_item_to_form(object $tool, object $typeconfig, object $item): stdClass {
1426
    global $OUTPUT;
1427
 
1428
    $config = new stdClass();
1429
    $config->name = '';
1430
    if (isset($item->title)) {
1431
        $config->name = $item->title;
1432
    }
1433
    if (empty($config->name)) {
1434
        $config->name = $tool->name;
1435
    }
1436
    if (isset($item->text)) {
1437
        $config->introeditor = [
1438
            'text' => $item->text,
1439
            'format' => FORMAT_PLAIN
1440
        ];
1441
    } else {
1442
        $config->introeditor = [
1443
            'text' => '',
1444
            'format' => FORMAT_PLAIN
1445
        ];
1446
    }
1447
    if (isset($item->icon->{'@id'})) {
1448
        $iconurl = new moodle_url($item->icon->{'@id'});
1449
        // Assign item's icon URL to secureicon or icon depending on its scheme.
1450
        if (strtolower($iconurl->get_scheme()) === 'https') {
1451
            $config->secureicon = $iconurl->out(false);
1452
        } else {
1453
            $config->icon = $iconurl->out(false);
1454
        }
1455
    }
1456
    if (isset($item->url)) {
1457
        $url = new moodle_url($item->url);
1458
        $config->toolurl = $url->out(false);
1459
        $config->typeid = 0;
1460
    } else {
1461
        $config->typeid = $tool->id;
1462
    }
1463
    $config->instructorchoiceacceptgrades = LTI_SETTING_NEVER;
1464
    $islti2 = $tool->ltiversion === LTI_VERSION_2;
1465
    if (!$islti2 && isset($typeconfig->lti_acceptgrades)) {
1466
        $acceptgrades = $typeconfig->lti_acceptgrades;
1467
        if ($acceptgrades == LTI_SETTING_ALWAYS) {
1468
            // We create a line item regardless if the definition contains one or not.
1469
            $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
1470
            $config->grade_modgrade_point = 100;
1471
        }
1472
        if ($acceptgrades == LTI_SETTING_DELEGATE || $acceptgrades == LTI_SETTING_ALWAYS) {
1473
            if (isset($item->lineItem)) {
1474
                $lineitem = $item->lineItem;
1475
                $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
1476
                $maxscore = 100;
1477
                if (isset($lineitem->scoreConstraints)) {
1478
                    $sc = $lineitem->scoreConstraints;
1479
                    if (isset($sc->totalMaximum)) {
1480
                        $maxscore = $sc->totalMaximum;
1481
                    } else if (isset($sc->normalMaximum)) {
1482
                        $maxscore = $sc->normalMaximum;
1483
                    }
1484
                }
1485
                $config->grade_modgrade_point = $maxscore;
1486
                $config->lineitemresourceid = '';
1487
                $config->lineitemtag = '';
1488
                $config->lineitemsubreviewurl = '';
1489
                $config->lineitemsubreviewparams = '';
1490
                if (isset($lineitem->assignedActivity) && isset($lineitem->assignedActivity->activityId)) {
1491
                    $config->lineitemresourceid = $lineitem->assignedActivity->activityId?:'';
1492
                }
1493
                if (isset($lineitem->tag)) {
1494
                    $config->lineitemtag = $lineitem->tag?:'';
1495
                }
1496
                if (isset($lineitem->submissionReview)) {
1497
                    $subreview = $lineitem->submissionReview;
1498
                    $config->lineitemsubreviewurl = 'DEFAULT';
1499
                    if (!empty($subreview->url)) {
1500
                        $config->lineitemsubreviewurl = $subreview->url;
1501
                    }
1502
                    if (isset($subreview->custom)) {
1503
                        $config->lineitemsubreviewparams = params_to_string($subreview->custom);
1504
                    }
1505
                }
1506
            }
1507
        }
1508
    }
1509
    $config->instructorchoicesendname = LTI_SETTING_NEVER;
1510
    $config->instructorchoicesendemailaddr = LTI_SETTING_NEVER;
1511
 
1512
    // Since 4.3, the launch container is dictated by the value set in tool configuration and isn't controllable by content items.
1513
    $config->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
1514
 
1515
    if (isset($item->custom)) {
1516
        $config->instructorcustomparameters = params_to_string($item->custom);
1517
    }
1518
 
1519
    // Set the status, allowing the form to validate, and pass an indicator to the relevant form field.
1520
    $config->selectcontentstatus = true;
1521
    $config->selectcontentindicator = $OUTPUT->pix_icon('i/valid', get_string('yes')) . get_string('contentselected', 'mod_lti');
1522
 
1523
    return $config;
1524
}
1525
 
1526
/**
1527
 * Processes the tool provider's response to the ContentItemSelectionRequest and builds the configuration data from the
1528
 * selected content item. This configuration data can be then used when adding a tool into the course.
1529
 *
1530
 * @param int $typeid The tool type ID.
1531
 * @param string $messagetype The value for the lti_message_type parameter.
1532
 * @param string $ltiversion The value for the lti_version parameter.
1533
 * @param string $consumerkey The consumer key.
1534
 * @param string $contentitemsjson The JSON string for the content_items parameter.
1535
 * @return stdClass The array of module information objects.
1536
 * @throws moodle_exception
1537
 * @throws lti\OAuthException
1538
 */
1539
function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiversion, $consumerkey, $contentitemsjson) {
1540
    $tool = lti_get_type($typeid);
1541
    // Validate parameters.
1542
    if (!$tool) {
1543
        throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1544
    }
1545
    // Check lti_message_type. Show debugging if it's not set to ContentItemSelection.
1546
    // No need to throw exceptions for now since lti_message_type does not seem to be used in this processing at the moment.
1547
    if ($messagetype !== 'ContentItemSelection') {
1548
        debugging("lti_message_type is invalid: {$messagetype}. It should be set to 'ContentItemSelection'.",
1549
            DEBUG_DEVELOPER);
1550
    }
1551
 
1552
    // Check LTI versions from our side and the response's side. Show debugging if they don't match.
1553
    // No need to throw exceptions for now since LTI version does not seem to be used in this processing at the moment.
1554
    $expectedversion = $tool->ltiversion;
1555
    $islti2 = ($expectedversion === LTI_VERSION_2);
1556
    if ($ltiversion !== $expectedversion) {
1557
        debugging("lti_version from response does not match the tool's configuration. Tool: {$expectedversion}," .
1558
            " Response: {$ltiversion}", DEBUG_DEVELOPER);
1559
    }
1560
 
1561
    $items = json_decode($contentitemsjson);
1562
    if (empty($items)) {
1563
        throw new moodle_exception('errorinvaliddata', 'mod_lti', '', $contentitemsjson);
1564
    }
1565
    if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'})) {
1566
        throw new moodle_exception('errorinvalidresponseformat', 'mod_lti');
1567
    }
1568
 
1569
    $config = null;
1570
    $items = $items->{'@graph'};
1571
    if (!empty($items)) {
1572
        $typeconfig = lti_get_type_type_config($tool->id);
1573
        if (count($items) == 1) {
1574
            $config = content_item_to_form($tool, $typeconfig, $items[0]);
1575
        } else {
1576
            $multiple = [];
1577
            foreach ($items as $item) {
1578
                $multiple[] = content_item_to_form($tool, $typeconfig, $item);
1579
            }
1580
            $config = new stdClass();
1581
            $config->multiple = $multiple;
1582
        }
1583
    }
1584
    return $config;
1585
}
1586
 
1587
/**
1588
 * Converts the new Deep-Linking format for Content-Items to the old format.
1589
 *
1590
 * @param string $param JSON string representing new Deep-Linking format
1591
 * @return string  JSON representation of content-items
1592
 */
1593
function lti_convert_content_items($param) {
1594
    $items = array();
1595
    $json = json_decode($param);
1596
    if (!empty($json) && is_array($json)) {
1597
        foreach ($json as $item) {
1598
            if (isset($item->type)) {
1599
                $newitem = clone $item;
1600
                switch ($item->type) {
1601
                    case 'ltiResourceLink':
1602
                        $newitem->{'@type'} = 'LtiLinkItem';
1603
                        $newitem->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
1604
                        break;
1605
                    case 'link':
1606
                    case 'rich':
1607
                        $newitem->{'@type'} = 'ContentItem';
1608
                        $newitem->mediaType = 'text/html';
1609
                        break;
1610
                    case 'file':
1611
                        $newitem->{'@type'} = 'FileItem';
1612
                        break;
1613
                }
1614
                unset($newitem->type);
1615
                if (isset($item->html)) {
1616
                    $newitem->text = $item->html;
1617
                    unset($newitem->html);
1618
                }
1619
                if (isset($item->iframe)) {
1620
                    // DeepLinking allows multiple options to be declared as supported.
1621
                    // We favor iframe over new window if both are specified.
1622
                    $newitem->placementAdvice = new stdClass();
1623
                    $newitem->placementAdvice->presentationDocumentTarget = 'iframe';
1624
                    if (isset($item->iframe->width)) {
1625
                        $newitem->placementAdvice->displayWidth = $item->iframe->width;
1626
                    }
1627
                    if (isset($item->iframe->height)) {
1628
                        $newitem->placementAdvice->displayHeight = $item->iframe->height;
1629
                    }
1630
                    unset($newitem->iframe);
1631
                    unset($newitem->window);
1632
                } else if (isset($item->window)) {
1633
                    $newitem->placementAdvice = new stdClass();
1634
                    $newitem->placementAdvice->presentationDocumentTarget = 'window';
1635
                    if (isset($item->window->targetName)) {
1636
                        $newitem->placementAdvice->windowTarget = $item->window->targetName;
1637
                    }
1638
                    if (isset($item->window->width)) {
1639
                        $newitem->placementAdvice->displayWidth = $item->window->width;
1640
                    }
1641
                    if (isset($item->window->height)) {
1642
                        $newitem->placementAdvice->displayHeight = $item->window->height;
1643
                    }
1644
                    unset($newitem->window);
1645
                } else if (isset($item->presentation)) {
1646
                    // This may have been part of an early draft but is not in the final spec
1647
                    // so keeping it around for now in case it's actually been used.
1648
                    $newitem->placementAdvice = new stdClass();
1649
                    if (isset($item->presentation->documentTarget)) {
1650
                        $newitem->placementAdvice->presentationDocumentTarget = $item->presentation->documentTarget;
1651
                    }
1652
                    if (isset($item->presentation->windowTarget)) {
1653
                        $newitem->placementAdvice->windowTarget = $item->presentation->windowTarget;
1654
                    }
1655
                    if (isset($item->presentation->width)) {
1656
                        $newitem->placementAdvice->dislayWidth = $item->presentation->width;
1657
                    }
1658
                    if (isset($item->presentation->height)) {
1659
                        $newitem->placementAdvice->dislayHeight = $item->presentation->height;
1660
                    }
1661
                    unset($newitem->presentation);
1662
                }
1663
                if (isset($item->icon) && isset($item->icon->url)) {
1664
                    $newitem->icon->{'@id'} = $item->icon->url;
1665
                    unset($newitem->icon->url);
1666
                }
1667
                if (isset($item->thumbnail) && isset($item->thumbnail->url)) {
1668
                    $newitem->thumbnail->{'@id'} = $item->thumbnail->url;
1669
                    unset($newitem->thumbnail->url);
1670
                }
1671
                if (isset($item->lineItem)) {
1672
                    unset($newitem->lineItem);
1673
                    $newitem->lineItem = new stdClass();
1674
                    $newitem->lineItem->{'@type'} = 'LineItem';
1675
                    $newitem->lineItem->reportingMethod = 'http://purl.imsglobal.org/ctx/lis/v2p1/Result#totalScore';
1676
                    if (isset($item->lineItem->label)) {
1677
                        $newitem->lineItem->label = $item->lineItem->label;
1678
                    }
1679
                    if (isset($item->lineItem->resourceId)) {
1680
                        $newitem->lineItem->assignedActivity = new stdClass();
1681
                        $newitem->lineItem->assignedActivity->activityId = $item->lineItem->resourceId;
1682
                    }
1683
                    if (isset($item->lineItem->tag)) {
1684
                        $newitem->lineItem->tag = $item->lineItem->tag;
1685
                    }
1686
                    if (isset($item->lineItem->scoreMaximum)) {
1687
                        $newitem->lineItem->scoreConstraints = new stdClass();
1688
                        $newitem->lineItem->scoreConstraints->{'@type'} = 'NumericLimits';
1689
                        $newitem->lineItem->scoreConstraints->totalMaximum = $item->lineItem->scoreMaximum;
1690
                    }
1691
                    if (isset($item->lineItem->submissionReview)) {
1692
                        $newitem->lineItem->submissionReview = $item->lineItem->submissionReview;
1693
                    }
1694
                }
1695
                $items[] = $newitem;
1696
            }
1697
        }
1698
    }
1699
 
1700
    $newitems = new stdClass();
1701
    $newitems->{'@context'} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem';
1702
    $newitems->{'@graph'} = $items;
1703
 
1704
    return json_encode($newitems);
1705
}
1706
 
1707
function lti_get_tool_table($tools, $id) {
1708
    global $OUTPUT;
1709
    $html = '';
1710
 
1711
    $typename = get_string('typename', 'lti');
1712
    $baseurl = get_string('baseurl', 'lti');
1713
    $action = get_string('action', 'lti');
1714
    $createdon = get_string('createdon', 'lti');
1715
 
1716
    if (!empty($tools)) {
1717
        $html .= "
1718
        <div id=\"{$id}_tools_container\" style=\"margin-top:.5em;margin-bottom:.5em\">
1719
            <table id=\"{$id}_tools\">
1720
                <thead>
1721
                    <tr>
1722
                        <th>$typename</th>
1723
                        <th>$baseurl</th>
1724
                        <th>$createdon</th>
1725
                        <th>$action</th>
1726
                    </tr>
1727
                </thead>
1728
        ";
1729
 
1730
        foreach ($tools as $type) {
1731
            $date = userdate($type->timecreated, get_string('strftimedatefullshort', 'core_langconfig'));
1732
            $accept = get_string('accept', 'lti');
1733
            $update = get_string('update', 'lti');
1734
            $delete = get_string('delete', 'lti');
1735
 
1736
            if (empty($type->toolproxyid)) {
1737
                $baseurl = new \moodle_url('/mod/lti/typessettings.php', array(
1738
                        'action' => 'accept',
1739
                        'id' => $type->id,
1740
                        'sesskey' => sesskey(),
1741
                        'tab' => $id
1742
                    ));
1743
                $ref = $type->baseurl;
1744
            } else {
1745
                $baseurl = new \moodle_url('/mod/lti/toolssettings.php', array(
1746
                        'action' => 'accept',
1747
                        'id' => $type->id,
1748
                        'sesskey' => sesskey(),
1749
                        'tab' => $id
1750
                    ));
1751
                $ref = $type->tpname;
1752
            }
1753
 
1754
            $accepthtml = $OUTPUT->action_icon($baseurl,
1755
                    new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null,
1756
                    array('title' => $accept, 'class' => 'editing_accept'));
1757
 
1758
            $deleteaction = 'delete';
1759
 
1760
            if ($type->state == LTI_TOOL_STATE_CONFIGURED) {
1761
                $accepthtml = '';
1762
            }
1763
 
1764
            if ($type->state != LTI_TOOL_STATE_REJECTED) {
1765
                $deleteaction = 'reject';
1766
                $delete = get_string('reject', 'lti');
1767
            }
1768
 
1769
            $updateurl = clone($baseurl);
1770
            $updateurl->param('action', 'update');
1771
            $updatehtml = $OUTPUT->action_icon($updateurl,
1772
                    new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null,
1773
                    array('title' => $update, 'class' => 'editing_update'));
1774
 
1775
            if (($type->state != LTI_TOOL_STATE_REJECTED) || empty($type->toolproxyid)) {
1776
                $deleteurl = clone($baseurl);
1777
                $deleteurl->param('action', $deleteaction);
1778
                $deletehtml = $OUTPUT->action_icon($deleteurl,
1779
                        new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null,
1780
                        array('title' => $delete, 'class' => 'editing_delete'));
1781
            } else {
1782
                $deletehtml = '';
1783
            }
1784
            $html .= "
1785
            <tr>
1786
                <td>
1787
                    {$type->name}
1788
                </td>
1789
                <td>
1790
                    {$ref}
1791
                </td>
1792
                <td>
1793
                    {$date}
1794
                </td>
1795
                <td align=\"center\">
1796
                    {$accepthtml}{$updatehtml}{$deletehtml}
1797
                </td>
1798
            </tr>
1799
            ";
1800
        }
1801
        $html .= '</table></div>';
1802
    } else {
1803
        $html .= get_string('no_' . $id, 'lti');
1804
    }
1805
 
1806
    return $html;
1807
}
1808
 
1809
/**
1810
 * This function builds the tab for a category of tool proxies
1811
 *
1812
 * @param object    $toolproxies    Tool proxy instance objects
1813
 * @param string    $id             Category ID
1814
 *
1815
 * @return string                   HTML for tab
1816
 */
1817
function lti_get_tool_proxy_table($toolproxies, $id) {
1818
    global $OUTPUT;
1819
 
1820
    if (!empty($toolproxies)) {
1821
        $typename = get_string('typename', 'lti');
1822
        $url = get_string('registrationurl', 'lti');
1823
        $action = get_string('action', 'lti');
1824
        $createdon = get_string('createdon', 'lti');
1825
 
1826
        $html = <<< EOD
1827
        <div id="{$id}_tool_proxies_container" style="margin-top: 0.5em; margin-bottom: 0.5em">
1828
            <table id="{$id}_tool_proxies">
1829
                <thead>
1830
                    <tr>
1831
                        <th>{$typename}</th>
1832
                        <th>{$url}</th>
1833
                        <th>{$createdon}</th>
1834
                        <th>{$action}</th>
1835
                    </tr>
1836
                </thead>
1837
EOD;
1838
        foreach ($toolproxies as $toolproxy) {
1839
            $date = userdate($toolproxy->timecreated, get_string('strftimedatefullshort', 'core_langconfig'));
1840
            $accept = get_string('register', 'lti');
1841
            $update = get_string('update', 'lti');
1842
            $delete = get_string('delete', 'lti');
1843
 
1844
            $baseurl = new \moodle_url('/mod/lti/registersettings.php', array(
1845
                    'action' => 'accept',
1846
                    'id' => $toolproxy->id,
1847
                    'sesskey' => sesskey(),
1848
                    'tab' => $id
1849
                ));
1850
 
1851
            $registerurl = new \moodle_url('/mod/lti/register.php', array(
1852
                    'id' => $toolproxy->id,
1853
                    'sesskey' => sesskey(),
1854
                    'tab' => 'tool_proxy'
1855
                ));
1856
 
1857
            $accepthtml = $OUTPUT->action_icon($registerurl,
1858
                    new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null,
1859
                    array('title' => $accept, 'class' => 'editing_accept'));
1860
 
1861
            $deleteaction = 'delete';
1862
 
1863
            if ($toolproxy->state != LTI_TOOL_PROXY_STATE_CONFIGURED) {
1864
                $accepthtml = '';
1865
            }
1866
 
1867
            if (($toolproxy->state == LTI_TOOL_PROXY_STATE_CONFIGURED) || ($toolproxy->state == LTI_TOOL_PROXY_STATE_PENDING)) {
1868
                $delete = get_string('cancel', 'lti');
1869
            }
1870
 
1871
            $updateurl = clone($baseurl);
1872
            $updateurl->param('action', 'update');
1873
            $updatehtml = $OUTPUT->action_icon($updateurl,
1874
                    new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null,
1875
                    array('title' => $update, 'class' => 'editing_update'));
1876
 
1877
            $deleteurl = clone($baseurl);
1878
            $deleteurl->param('action', $deleteaction);
1879
            $deletehtml = $OUTPUT->action_icon($deleteurl,
1880
                    new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null,
1881
                    array('title' => $delete, 'class' => 'editing_delete'));
1882
            $html .= <<< EOD
1883
            <tr>
1884
                <td>
1885
                    {$toolproxy->name}
1886
                </td>
1887
                <td>
1888
                    {$toolproxy->regurl}
1889
                </td>
1890
                <td>
1891
                    {$date}
1892
                </td>
1893
                <td align="center">
1894
                    {$accepthtml}{$updatehtml}{$deletehtml}
1895
                </td>
1896
            </tr>
1897
EOD;
1898
        }
1899
        $html .= '</table></div>';
1900
    } else {
1901
        $html = get_string('no_' . $id, 'lti');
1902
    }
1903
 
1904
    return $html;
1905
}
1906
 
1907
/**
1908
 * Extracts the enabled capabilities into an array, including those implicitly declared in a parameter
1909
 *
1910
 * @param object $tool  Tool instance object
1911
 *
1912
 * @return array List of enabled capabilities
1913
 */
1914
function lti_get_enabled_capabilities($tool) {
1915
    if (!isset($tool)) {
1916
        return array();
1917
    }
1918
    if (!empty($tool->enabledcapability)) {
1919
        $enabledcapabilities = explode("\n", $tool->enabledcapability);
1920
    } else {
1921
        $enabledcapabilities = array();
1922
    }
1923
    if (!empty($tool->parameter)) {
1924
        $paramstr = str_replace("\r\n", "\n", $tool->parameter);
1925
        $paramstr = str_replace("\n\r", "\n", $paramstr);
1926
        $paramstr = str_replace("\r", "\n", $paramstr);
1927
        $params = explode("\n", $paramstr);
1928
        foreach ($params as $param) {
1929
            $pos = strpos($param, '=');
1930
            if (($pos === false) || ($pos < 1)) {
1931
                continue;
1932
            }
1933
            $value = trim(core_text::substr($param, $pos + 1, strlen($param)));
1934
            if (substr($value, 0, 1) == '$') {
1935
                $value = substr($value, 1);
1936
                if (!in_array($value, $enabledcapabilities)) {
1937
                    $enabledcapabilities[] = $value;
1938
                }
1939
            }
1940
        }
1941
    }
1942
    return $enabledcapabilities;
1943
}
1944
 
1945
/**
1946
 * Splits the custom parameters
1947
 *
1948
 * @param string    $customstr      String containing the parameters
1949
 *
1950
 * @return array of custom parameters
1951
 */
1952
function lti_split_parameters($customstr) {
1953
    $customstr = str_replace("\r\n", "\n", $customstr);
1954
    $customstr = str_replace("\n\r", "\n", $customstr);
1955
    $customstr = str_replace("\r", "\n", $customstr);
1956
    $lines = explode("\n", $customstr);  // Or should this split on "/[\n;]/"?
1957
    $retval = array();
1958
    foreach ($lines as $line) {
1959
        $pos = strpos($line, '=');
1960
        if ( $pos === false || $pos < 1 ) {
1961
            continue;
1962
        }
1963
        $key = trim(core_text::substr($line, 0, $pos));
1964
        $val = trim(core_text::substr($line, $pos + 1, strlen($line)));
1965
        $retval[$key] = $val;
1966
    }
1967
    return $retval;
1968
}
1969
 
1970
/**
1971
 * Splits the custom parameters field to the various parameters
1972
 *
1973
 * @param object    $toolproxy      Tool proxy instance object
1974
 * @param object    $tool           Tool instance object
1975
 * @param array     $params         LTI launch parameters
1976
 * @param string    $customstr      String containing the parameters
1977
 * @param boolean   $islti2         True if an LTI 2 tool is being launched
1978
 *
1979
 * @return array of custom parameters
1980
 */
1981
function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2 = false) {
1982
    $splitted = lti_split_parameters($customstr);
1983
    $retval = array();
1984
    foreach ($splitted as $key => $val) {
1985
        $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, $islti2);
1986
        $key2 = lti_map_keyname($key);
1987
        $retval['custom_'.$key2] = $val;
1988
        if (($islti2 || ($tool->ltiversion === LTI_VERSION_1P3)) && ($key != $key2)) {
1989
            $retval['custom_'.$key] = $val;
1990
        }
1991
    }
1992
    return $retval;
1993
}
1994
 
1995
/**
1996
 * Adds the custom parameters to an array
1997
 *
1998
 * @param object    $toolproxy      Tool proxy instance object
1999
 * @param object    $tool           Tool instance object
2000
 * @param array     $params         LTI launch parameters
2001
 * @param array     $parameters     Array containing the parameters
2002
 *
2003
 * @return array    Array of custom parameters
2004
 */
2005
function lti_get_custom_parameters($toolproxy, $tool, $params, $parameters) {
2006
    $retval = array();
2007
    foreach ($parameters as $key => $val) {
2008
        $key2 = lti_map_keyname($key);
2009
        $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, true);
2010
        $retval['custom_'.$key2] = $val;
2011
        if ($key != $key2) {
2012
            $retval['custom_'.$key] = $val;
2013
        }
2014
    }
2015
    return $retval;
2016
}
2017
 
2018
/**
2019
 * Parse a custom parameter to replace any substitution variables
2020
 *
2021
 * @param object    $toolproxy      Tool proxy instance object
2022
 * @param object    $tool           Tool instance object
2023
 * @param array     $params         LTI launch parameters
2024
 * @param string    $value          Custom parameter value
2025
 * @param boolean   $islti2         True if an LTI 2 tool is being launched
2026
 *
2027
 * @return string Parsed value of custom parameter
2028
 */
2029
function lti_parse_custom_parameter($toolproxy, $tool, $params, $value, $islti2) {
2030
    // This is required as {${$valarr[0]}->{$valarr[1]}}" may be using the USER or COURSE var.
2031
    global $USER, $COURSE;
2032
 
2033
    if ($value) {
2034
        if (substr($value, 0, 1) == '\\') {
2035
            $value = substr($value, 1);
2036
        } else if (substr($value, 0, 1) == '$') {
2037
            $value1 = substr($value, 1);
2038
            $enabledcapabilities = lti_get_enabled_capabilities($tool);
2039
            if (!$islti2 || in_array($value1, $enabledcapabilities)) {
2040
                $capabilities = lti_get_capabilities();
2041
                if (array_key_exists($value1, $capabilities)) {
2042
                    $val = $capabilities[$value1];
2043
                    if ($val) {
2044
                        if (substr($val, 0, 1) != '$') {
2045
                            $value = $params[$val];
2046
                        } else {
2047
                            $valarr = explode('->', substr($val, 1), 2);
2048
                            $value = "{${$valarr[0]}->{$valarr[1]}}";
2049
                            $value = str_replace('<br />' , ' ', $value);
2050
                            $value = str_replace('<br>' , ' ', $value);
2051
                            $value = format_string($value);
2052
                        }
2053
                    } else {
2054
                        $value = lti_calculate_custom_parameter($value1);
2055
                    }
2056
                } else {
2057
                    $val = $value;
2058
                    $services = lti_get_services();
2059
                    foreach ($services as $service) {
2060
                        $service->set_tool_proxy($toolproxy);
2061
                        $service->set_type($tool);
2062
                        $value = $service->parse_value($val);
2063
                        if ($val != $value) {
2064
                            break;
2065
                        }
2066
                    }
2067
                }
2068
            }
2069
        }
2070
    }
2071
    return $value;
2072
}
2073
 
2074
/**
2075
 * Calculates the value of a custom parameter that has not been specified earlier
2076
 *
2077
 * @param string    $value          Custom parameter value
2078
 *
2079
 * @return string Calculated value of custom parameter
2080
 */
2081
function lti_calculate_custom_parameter($value) {
2082
    global $USER, $COURSE;
2083
 
2084
    switch ($value) {
2085
        case 'Moodle.Person.userGroupIds':
2086
            return implode(",", groups_get_user_groups($COURSE->id, $USER->id)[0]);
2087
        case 'Context.id.history':
2088
            return implode(",", get_course_history($COURSE));
2089
        case 'CourseSection.timeFrame.begin':
2090
            if (empty($COURSE->startdate)) {
2091
                return "";
2092
            }
2093
            $dt = new DateTime("@$COURSE->startdate", new DateTimeZone('UTC'));
2094
            return $dt->format(DateTime::ATOM);
2095
        case 'CourseSection.timeFrame.end':
2096
            if (empty($COURSE->enddate)) {
2097
                return "";
2098
            }
2099
            $dt = new DateTime("@$COURSE->enddate", new DateTimeZone('UTC'));
2100
            return $dt->format(DateTime::ATOM);
2101
    }
2102
    return null;
2103
}
2104
 
2105
/**
2106
 * Build the history chain for this course using the course originalcourseid.
2107
 *
2108
 * @param object $course course for which the history is returned.
2109
 *
2110
 * @return array ids of the source course in ancestry order, immediate parent 1st.
2111
 */
2112
function get_course_history($course) {
2113
    global $DB;
2114
    $history = [];
2115
    $parentid = $course->originalcourseid;
2116
    while (!empty($parentid) && !in_array($parentid, $history)) {
2117
        $history[] = $parentid;
2118
        $parentid = $DB->get_field('course', 'originalcourseid', array('id' => $parentid));
2119
    }
2120
    return $history;
2121
}
2122
 
2123
/**
2124
 * Used for building the names of the different custom parameters
2125
 *
2126
 * @param string $key   Parameter name
2127
 * @param bool $tolower Do we want to convert the key into lower case?
2128
 * @return string       Processed name
2129
 */
2130
function lti_map_keyname($key, $tolower = true) {
2131
    if ($tolower) {
2132
        $newkey = '';
2133
        $key = core_text::strtolower(trim($key));
2134
        foreach (str_split($key) as $ch) {
2135
            if ( ($ch >= 'a' && $ch <= 'z') || ($ch >= '0' && $ch <= '9') ) {
2136
                $newkey .= $ch;
2137
            } else {
2138
                $newkey .= '_';
2139
            }
2140
        }
2141
    } else {
2142
        $newkey = $key;
2143
    }
2144
    return $newkey;
2145
}
2146
 
2147
/**
2148
 * Gets the IMS role string for the specified user and LTI course module.
2149
 *
2150
 * @param mixed    $user      User object or user id
2151
 * @param int      $cmid      The course module id of the LTI activity
2152
 * @param int      $courseid  The course id of the LTI activity
2153
 * @param boolean  $islti2    True if an LTI 2 tool is being launched
2154
 *
2155
 * @return string A role string suitable for passing with an LTI launch
2156
 */
2157
function lti_get_ims_role($user, $cmid, $courseid, $islti2) {
2158
    $roles = array();
2159
 
2160
    if (empty($cmid)) {
2161
        // If no cmid is passed, check if the user is a teacher in the course
2162
        // This allows other modules to programmatically "fake" a launch without
2163
        // a real LTI instance.
2164
        $context = context_course::instance($courseid);
2165
 
2166
        if (has_capability('moodle/course:manageactivities', $context, $user)) {
2167
            array_push($roles, 'Instructor');
2168
        } else {
2169
            array_push($roles, 'Learner');
2170
        }
2171
    } else {
2172
        $context = context_module::instance($cmid);
2173
 
2174
        if (has_capability('mod/lti:manage', $context)) {
2175
            array_push($roles, 'Instructor');
2176
        } else {
2177
            array_push($roles, 'Learner');
2178
        }
2179
    }
2180
 
2181
    if (!is_role_switched($courseid) && (is_siteadmin($user)) || has_capability('mod/lti:admin', $context)) {
2182
        // Make sure admins do not have the Learner role, then set admin role.
2183
        $roles = array_diff($roles, array('Learner'));
2184
        if (!$islti2) {
2185
            array_push($roles, 'urn:lti:sysrole:ims/lis/Administrator', 'urn:lti:instrole:ims/lis/Administrator');
2186
        } else {
2187
            array_push($roles, 'http://purl.imsglobal.org/vocab/lis/v2/person#Administrator');
2188
        }
2189
    }
2190
 
2191
    return join(',', $roles);
2192
}
2193
 
2194
/**
2195
 * Returns configuration details for the tool
2196
 *
2197
 * @param int $typeid   Basic LTI tool typeid
2198
 *
2199
 * @return array        Tool Configuration
2200
 */
2201
function lti_get_type_config($typeid) {
2202
    global $DB;
2203
 
2204
    $query = "SELECT name, value
2205
                FROM {lti_types_config}
2206
               WHERE typeid = :typeid1
2207
           UNION ALL
2208
              SELECT 'toolurl' AS name, baseurl AS value
2209
                FROM {lti_types}
2210
               WHERE id = :typeid2
2211
           UNION ALL
2212
              SELECT 'icon' AS name, icon AS value
2213
                FROM {lti_types}
2214
               WHERE id = :typeid3
2215
           UNION ALL
2216
              SELECT 'secureicon' AS name, secureicon AS value
2217
                FROM {lti_types}
2218
               WHERE id = :typeid4";
2219
 
2220
    $typeconfig = array();
2221
    $configs = $DB->get_records_sql($query,
2222
        array('typeid1' => $typeid, 'typeid2' => $typeid, 'typeid3' => $typeid, 'typeid4' => $typeid));
2223
 
2224
    if (!empty($configs)) {
2225
        foreach ($configs as $config) {
2226
            $typeconfig[$config->name] = $config->value;
2227
        }
2228
    }
2229
 
2230
    return $typeconfig;
2231
}
2232
 
2233
function lti_get_tools_by_url($url, $state, $courseid = null) {
2234
    $domain = lti_get_domain_from_url($url);
2235
 
2236
    return lti_get_tools_by_domain($domain, $state, $courseid);
2237
}
2238
 
2239
function lti_get_tools_by_domain($domain, $state = null, $courseid = null) {
2240
    global $DB, $SITE;
2241
 
2242
    $statefilter = '';
2243
    $coursefilter = '';
2244
 
2245
    if ($state) {
2246
        $statefilter = 'AND t.state = :state';
2247
    }
2248
 
2249
    if ($courseid && $courseid != $SITE->id) {
2250
        $coursefilter = 'OR t.course = :courseid';
2251
    }
2252
 
2253
    $coursecategory = $DB->get_field('course', 'category', ['id' => $courseid]);
2254
    $query = "SELECT t.*
2255
                FROM {lti_types} t
2256
           LEFT JOIN {lti_types_categories} tc on t.id = tc.typeid
2257
               WHERE t.tooldomain = :tooldomain
2258
                 AND (t.course = :siteid $coursefilter)
2259
                 $statefilter
2260
                 AND (tc.id IS NULL OR tc.categoryid = :categoryid)";
2261
 
2262
    return $DB->get_records_sql($query, [
2263
            'courseid' => $courseid,
2264
            'siteid' => $SITE->id,
2265
            'tooldomain' => $domain,
2266
            'state' => $state,
2267
            'categoryid' => $coursecategory
2268
        ]);
2269
}
2270
 
2271
/**
2272
 * Returns all basicLTI tools configured by the administrator
2273
 *
2274
 * @param int $course
2275
 *
2276
 * @return array
2277
 */
2278
function lti_filter_get_types($course) {
2279
    global $DB;
2280
 
2281
    if (!empty($course)) {
2282
        $where = "WHERE t.course = :course";
2283
        $params = array('course' => $course);
2284
    } else {
2285
        $where = '';
2286
        $params = array();
2287
    }
2288
    $query = "SELECT t.id, t.name, t.baseurl, t.state, t.toolproxyid, t.timecreated, tp.name tpname
2289
                FROM {lti_types} t LEFT OUTER JOIN {lti_tool_proxies} tp ON t.toolproxyid = tp.id
2290
                {$where}";
2291
    return $DB->get_records_sql($query, $params);
2292
}
2293
 
2294
/**
2295
 * Given an array of tools, filter them based on their state
2296
 *
2297
 * @param array $tools An array of lti_types records
2298
 * @param int $state One of the LTI_TOOL_STATE_* constants
2299
 * @return array
2300
 */
2301
function lti_filter_tool_types(array $tools, $state) {
2302
    $return = array();
2303
    foreach ($tools as $key => $tool) {
2304
        if ($tool->state == $state) {
2305
            $return[$key] = $tool;
2306
        }
2307
    }
2308
    return $return;
2309
}
2310
 
2311
/**
2312
 * Returns all lti types visible in this course
2313
 *
2314
 * @deprecated since Moodle 4.3
2315
 * @param int $courseid The id of the course to retieve types for
2316
 * @param array $coursevisible options for 'coursevisible' field,
2317
 *        default [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER]
2318
 * @return stdClass[] All the lti types visible in the given course
2319
 */
2320
function lti_get_lti_types_by_course($courseid, $coursevisible = null) {
2321
    debugging(__FUNCTION__ . '() is deprecated. Please use \mod_lti\local\types_helper::get_lti_types_by_course() instead.',
2322
        DEBUG_DEVELOPER);
2323
 
2324
    global $USER;
2325
    return \mod_lti\local\types_helper::get_lti_types_by_course($courseid, $USER->id, $coursevisible ?? []);
2326
}
2327
 
2328
/**
2329
 * Returns tool types for lti add instance and edit page
2330
 *
2331
 * @return array Array of lti types
2332
 */
2333
function lti_get_types_for_add_instance() {
2334
    global $COURSE, $USER;
2335
 
2336
    // Always return the 'manual' type option, despite manual config being deprecated, so that we have it for legacy instances.
2337
    $types = [(object) ['name' => get_string('automatic', 'lti'), 'course' => 0, 'toolproxyid' => null]];
2338
 
2339
    $preconfiguredtypes = \mod_lti\local\types_helper::get_lti_types_by_course($COURSE->id, $USER->id);
2340
    foreach ($preconfiguredtypes as $type) {
2341
        $types[$type->id] = $type;
2342
    }
2343
 
2344
    return $types;
2345
}
2346
 
2347
/**
2348
 * Returns a list of configured types in the given course
2349
 *
2350
 * @param int $courseid The id of the course to retieve types for
2351
 * @param int $sectionreturn section to return to for forming the URLs
2352
 * @return array Array of lti types. Each element is object with properties: name, title, icon, help, helplink, link
2353
 */
2354
function lti_get_configured_types($courseid, $sectionreturn = 0) {
2355
    global $OUTPUT, $USER;
2356
    $types = [];
2357
    $preconfiguredtypes = \mod_lti\local\types_helper::get_lti_types_by_course($courseid, $USER->id,
2358
        [LTI_COURSEVISIBLE_ACTIVITYCHOOSER]);
2359
 
2360
    foreach ($preconfiguredtypes as $ltitype) {
2361
        $type           = new stdClass();
2362
        $type->id       = $ltitype->id;
2363
        $type->modclass = MOD_CLASS_ACTIVITY;
2364
        $type->name     = 'lti_type_' . $ltitype->id;
2365
        // Clean the name. We don't want tags here.
2366
        $type->title    = clean_param($ltitype->name, PARAM_NOTAGS);
2367
        $trimmeddescription = trim($ltitype->description ?? '');
2368
        if ($trimmeddescription != '') {
2369
            // Clean the description. We don't want tags here.
2370
            $type->help     = clean_param($trimmeddescription, PARAM_NOTAGS);
2371
            $type->helplink = get_string('modulename_shortcut_link', 'lti');
2372
        }
2373
 
2374
        $iconurl = get_tool_type_icon_url($ltitype);
2375
        $iconclass = '';
2376
        if ($iconurl !== $OUTPUT->image_url('monologo', 'lti')->out()) {
2377
            // Do not filter the icon if it is not the default LTI activity icon.
2378
            $iconclass = 'nofilter';
2379
        }
2380
        $type->icon = html_writer::empty_tag('img', ['src' => $iconurl, 'alt' => '', 'class' => "icon $iconclass"]);
2381
 
2382
        $params = [
2383
            'add' => 'lti',
2384
            'return' => 0,
2385
            'course' => $courseid,
2386
            'typeid' => $ltitype->id,
2387
        ];
2388
        if (!is_null($sectionreturn)) {
2389
            $params['sr'] = $sectionreturn;
2390
        }
2391
        $type->link = new moodle_url('/course/modedit.php', $params);
2392
        $types[] = $type;
2393
    }
2394
    return $types;
2395
}
2396
 
2397
function lti_get_domain_from_url($url) {
2398
    $matches = array();
2399
 
2400
    if (preg_match(LTI_URL_DOMAIN_REGEX, $url ?? '', $matches)) {
2401
        return $matches[1];
2402
    }
2403
}
2404
 
2405
function lti_get_tool_by_url_match($url, $courseid = null, $state = LTI_TOOL_STATE_CONFIGURED) {
2406
    $possibletools = lti_get_tools_by_url($url, $state, $courseid);
2407
 
2408
    return lti_get_best_tool_by_url($url, $possibletools, $courseid);
2409
}
2410
 
2411
function lti_get_url_thumbprint($url) {
2412
    // Parse URL requires a schema otherwise everything goes into 'path'.  Fixed 5.4.7 or later.
2413
    if (preg_match('/https?:\/\//', $url) !== 1) {
2414
        $url = 'http://'.$url;
2415
    }
2416
    $urlparts = parse_url(strtolower($url));
2417
    if (!isset($urlparts['path'])) {
2418
        $urlparts['path'] = '';
2419
    }
2420
 
2421
    if (!isset($urlparts['query'])) {
2422
        $urlparts['query'] = '';
2423
    }
2424
 
2425
    if (!isset($urlparts['host'])) {
2426
        $urlparts['host'] = '';
2427
    }
2428
 
2429
    if (substr($urlparts['host'], 0, 4) === 'www.') {
2430
        $urlparts['host'] = substr($urlparts['host'], 4);
2431
    }
2432
 
2433
    $urllower = $urlparts['host'] . '/' . $urlparts['path'];
2434
 
2435
    if ($urlparts['query'] != '') {
2436
        $urllower .= '?' . $urlparts['query'];
2437
    }
2438
 
2439
    return $urllower;
2440
}
2441
 
2442
function lti_get_best_tool_by_url($url, $tools, $courseid = null) {
2443
    if (count($tools) === 0) {
2444
        return null;
2445
    }
2446
 
2447
    $urllower = lti_get_url_thumbprint($url);
2448
 
2449
    foreach ($tools as $tool) {
2450
        $tool->_matchscore = 0;
2451
 
2452
        $toolbaseurllower = lti_get_url_thumbprint($tool->baseurl);
2453
 
2454
        if ($urllower === $toolbaseurllower) {
2455
            // 100 points for exact thumbprint match.
2456
            $tool->_matchscore += 100;
2457
        } else if (substr($urllower, 0, strlen($toolbaseurllower)) === $toolbaseurllower) {
2458
            // 50 points if tool thumbprint starts with the base URL thumbprint.
2459
            $tool->_matchscore += 50;
2460
        }
2461
 
2462
        // Prefer course tools over site tools.
2463
        if (!empty($courseid)) {
2464
            // Minus 10 points for not matching the course id (global tools).
2465
            if ($tool->course != $courseid) {
2466
                $tool->_matchscore -= 10;
2467
            }
2468
        }
2469
    }
2470
 
2471
    $bestmatch = array_reduce($tools, function($value, $tool) {
2472
        if ($tool->_matchscore > $value->_matchscore) {
2473
            return $tool;
2474
        } else {
2475
            return $value;
2476
        }
2477
 
2478
    }, (object)array('_matchscore' => -1));
2479
 
2480
    // None of the tools are suitable for this URL.
2481
    if ($bestmatch->_matchscore <= 0) {
2482
        return null;
2483
    }
2484
 
2485
    return $bestmatch;
2486
}
2487
 
2488
function lti_get_shared_secrets_by_key($key) {
2489
    global $DB;
2490
 
2491
    // Look up the shared secret for the specified key in both the types_config table (for configured tools)
2492
    // And in the lti resource table for ad-hoc tools.
2493
    $lti13 = LTI_VERSION_1P3;
2494
    $query = "SELECT " . $DB->sql_compare_text('t2.value', 256) . " AS value
2495
                FROM {lti_types_config} t1
2496
                JOIN {lti_types_config} t2 ON t1.typeid = t2.typeid
2497
                JOIN {lti_types} type ON t2.typeid = type.id
2498
              WHERE t1.name = 'resourcekey'
2499
                AND " . $DB->sql_compare_text('t1.value', 256) . " = :key1
2500
                AND t2.name = 'password'
2501
                AND type.state = :configured1
2502
                AND type.ltiversion <> :ltiversion
2503
               UNION
2504
              SELECT tp.secret AS value
2505
                FROM {lti_tool_proxies} tp
2506
                JOIN {lti_types} t ON tp.id = t.toolproxyid
2507
              WHERE tp.guid = :key2
2508
                AND t.state = :configured2
2509
               UNION
2510
              SELECT password AS value
2511
               FROM {lti}
2512
              WHERE resourcekey = :key3";
2513
 
2514
    $sharedsecrets = $DB->get_records_sql($query, array('configured1' => LTI_TOOL_STATE_CONFIGURED, 'ltiversion' => $lti13,
2515
        'configured2' => LTI_TOOL_STATE_CONFIGURED, 'key1' => $key, 'key2' => $key, 'key3' => $key));
2516
 
2517
    $values = array_map(function($item) {
2518
        return $item->value;
2519
    }, $sharedsecrets);
2520
 
2521
    // There should really only be one shared secret per key. But, we can't prevent
2522
    // more than one getting entered. For instance, if the same key is used for two tool providers.
2523
    return $values;
2524
}
2525
 
2526
/**
2527
 * Delete a Basic LTI configuration
2528
 *
2529
 * @param int $id   Configuration id
2530
 */
2531
function lti_delete_type($id) {
2532
    global $DB;
2533
 
2534
    // We should probably just copy the launch URL to the tool instances in this case... using a single query.
2535
    /*
2536
    $instances = $DB->get_records('lti', array('typeid' => $id));
2537
    foreach ($instances as $instance) {
2538
        $instance->typeid = 0;
2539
        $DB->update_record('lti', $instance);
2540
    }*/
2541
 
2542
    $DB->delete_records('lti_types', array('id' => $id));
2543
    $DB->delete_records('lti_types_config', array('typeid' => $id));
2544
    $DB->delete_records('lti_types_categories', array('typeid' => $id));
2545
}
2546
 
2547
function lti_set_state_for_type($id, $state) {
2548
    global $DB;
2549
 
2550
    $DB->update_record('lti_types', (object)array('id' => $id, 'state' => $state));
2551
}
2552
 
2553
/**
2554
 * Transforms a basic LTI object to an array
2555
 *
2556
 * @param object $ltiobject    Basic LTI object
2557
 *
2558
 * @return array Basic LTI configuration details
2559
 */
2560
function lti_get_config($ltiobject) {
2561
    $typeconfig = (array)$ltiobject;
2562
    $additionalconfig = lti_get_type_config($ltiobject->typeid);
2563
    $typeconfig = array_merge($typeconfig, $additionalconfig);
2564
    return $typeconfig;
2565
}
2566
 
2567
/**
2568
 *
2569
 * Generates some of the tool configuration based on the instance details
2570
 *
2571
 * @param int $id
2572
 *
2573
 * @return object configuration
2574
 *
2575
 */
2576
function lti_get_type_config_from_instance($id) {
2577
    global $DB;
2578
 
2579
    $instance = $DB->get_record('lti', array('id' => $id));
2580
    $config = lti_get_config($instance);
2581
 
2582
    $type = new \stdClass();
2583
    $type->lti_fix = $id;
2584
    if (isset($config['toolurl'])) {
2585
        $type->lti_toolurl = $config['toolurl'];
2586
    }
2587
    if (isset($config['instructorchoicesendname'])) {
2588
        $type->lti_sendname = $config['instructorchoicesendname'];
2589
    }
2590
    if (isset($config['instructorchoicesendemailaddr'])) {
2591
        $type->lti_sendemailaddr = $config['instructorchoicesendemailaddr'];
2592
    }
2593
    if (isset($config['instructorchoiceacceptgrades'])) {
2594
        $type->lti_acceptgrades = $config['instructorchoiceacceptgrades'];
2595
    }
2596
    if (isset($config['instructorchoiceallowroster'])) {
2597
        $type->lti_allowroster = $config['instructorchoiceallowroster'];
2598
    }
2599
 
2600
    if (isset($config['instructorcustomparameters'])) {
2601
        $type->lti_allowsetting = $config['instructorcustomparameters'];
2602
    }
2603
    return $type;
2604
}
2605
 
2606
/**
2607
 * Generates some of the tool configuration based on the admin configuration details
2608
 *
2609
 * @param int $id
2610
 *
2611
 * @return stdClass Configuration details
2612
 */
2613
function lti_get_type_type_config($id) {
2614
    global $DB;
2615
 
2616
    $basicltitype = $DB->get_record('lti_types', array('id' => $id));
2617
    $config = lti_get_type_config($id);
2618
 
2619
    $type = new \stdClass();
2620
 
2621
    $type->lti_typename = $basicltitype->name;
2622
 
2623
    $type->typeid = $basicltitype->id;
2624
 
2625
    $type->course = $basicltitype->course;
2626
 
2627
    $type->toolproxyid = $basicltitype->toolproxyid;
2628
 
2629
    $type->lti_toolurl = $basicltitype->baseurl;
2630
 
2631
    $type->lti_ltiversion = $basicltitype->ltiversion;
2632
 
2633
    $type->lti_clientid = $basicltitype->clientid;
2634
    $type->lti_clientid_disabled = $type->lti_clientid;
2635
 
2636
    $type->lti_description = $basicltitype->description;
2637
 
2638
    $type->lti_parameters = $basicltitype->parameter;
2639
 
2640
    $type->lti_icon = $basicltitype->icon;
2641
 
2642
    $type->lti_secureicon = $basicltitype->secureicon;
2643
 
2644
    if (isset($config['resourcekey'])) {
2645
        $type->lti_resourcekey = $config['resourcekey'];
2646
    }
2647
    if (isset($config['password'])) {
2648
        $type->lti_password = $config['password'];
2649
    }
2650
    if (isset($config['publickey'])) {
2651
        $type->lti_publickey = $config['publickey'];
2652
    }
2653
    if (isset($config['publickeyset'])) {
2654
        $type->lti_publickeyset = $config['publickeyset'];
2655
    }
2656
    if (isset($config['keytype'])) {
2657
        $type->lti_keytype = $config['keytype'];
2658
    }
2659
    if (isset($config['initiatelogin'])) {
2660
        $type->lti_initiatelogin = $config['initiatelogin'];
2661
    }
2662
    if (isset($config['redirectionuris'])) {
2663
        $type->lti_redirectionuris = $config['redirectionuris'];
2664
    }
2665
 
2666
    if (isset($config['sendname'])) {
2667
        $type->lti_sendname = $config['sendname'];
2668
    }
2669
    if (isset($config['instructorchoicesendname'])) {
2670
        $type->lti_instructorchoicesendname = $config['instructorchoicesendname'];
2671
    }
2672
    if (isset($config['sendemailaddr'])) {
2673
        $type->lti_sendemailaddr = $config['sendemailaddr'];
2674
    }
2675
    if (isset($config['instructorchoicesendemailaddr'])) {
2676
        $type->lti_instructorchoicesendemailaddr = $config['instructorchoicesendemailaddr'];
2677
    }
2678
    if (isset($config['acceptgrades'])) {
2679
        $type->lti_acceptgrades = $config['acceptgrades'];
2680
    }
2681
    if (isset($config['instructorchoiceacceptgrades'])) {
2682
        $type->lti_instructorchoiceacceptgrades = $config['instructorchoiceacceptgrades'];
2683
    }
2684
    if (isset($config['allowroster'])) {
2685
        $type->lti_allowroster = $config['allowroster'];
2686
    }
2687
    if (isset($config['instructorchoiceallowroster'])) {
2688
        $type->lti_instructorchoiceallowroster = $config['instructorchoiceallowroster'];
2689
    }
2690
 
2691
    if (isset($config['customparameters'])) {
2692
        $type->lti_customparameters = $config['customparameters'];
2693
    }
2694
 
2695
    if (isset($config['forcessl'])) {
2696
        $type->lti_forcessl = $config['forcessl'];
2697
    }
2698
 
2699
    if (isset($config['organizationid_default'])) {
2700
        $type->lti_organizationid_default = $config['organizationid_default'];
2701
    } else {
2702
        // Tool was configured before this option was available and the default then was host.
2703
        $type->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEHOST;
2704
    }
2705
    if (isset($config['organizationid'])) {
2706
        $type->lti_organizationid = $config['organizationid'];
2707
    }
2708
    if (isset($config['organizationurl'])) {
2709
        $type->lti_organizationurl = $config['organizationurl'];
2710
    }
2711
    if (isset($config['organizationdescr'])) {
2712
        $type->lti_organizationdescr = $config['organizationdescr'];
2713
    }
2714
    if (isset($config['launchcontainer'])) {
2715
        $type->lti_launchcontainer = $config['launchcontainer'];
2716
    }
2717
 
2718
    if (isset($config['coursevisible'])) {
2719
        $type->lti_coursevisible = $config['coursevisible'];
2720
    }
2721
 
2722
    if (isset($config['contentitem'])) {
2723
        $type->lti_contentitem = $config['contentitem'];
2724
    }
2725
 
2726
    if (isset($config['toolurl_ContentItemSelectionRequest'])) {
2727
        $type->lti_toolurl_ContentItemSelectionRequest = $config['toolurl_ContentItemSelectionRequest'];
2728
    }
2729
 
2730
    if (isset($config['debuglaunch'])) {
2731
        $type->lti_debuglaunch = $config['debuglaunch'];
2732
    }
2733
 
2734
    if (isset($config['module_class_type'])) {
2735
        $type->lti_module_class_type = $config['module_class_type'];
2736
    }
2737
 
2738
    // Get the parameters from the LTI services.
2739
    foreach ($config as $name => $value) {
2740
        if (strpos($name, 'ltiservice_') === 0) {
2741
            $type->{$name} = $config[$name];
2742
        }
2743
    }
2744
 
2745
    return $type;
2746
}
2747
 
2748
function lti_prepare_type_for_save($type, $config) {
2749
    if (isset($config->lti_toolurl)) {
2750
        $type->baseurl = $config->lti_toolurl;
2751
        if (isset($config->lti_tooldomain)) {
2752
            $type->tooldomain = $config->lti_tooldomain;
2753
        } else {
2754
            $type->tooldomain = lti_get_domain_from_url($config->lti_toolurl);
2755
        }
2756
    }
2757
    if (isset($config->lti_description)) {
2758
        $type->description = $config->lti_description;
2759
    }
2760
    if (isset($config->lti_typename)) {
2761
        $type->name = $config->lti_typename;
2762
    }
2763
    if (isset($config->lti_ltiversion)) {
2764
        $type->ltiversion = $config->lti_ltiversion;
2765
    }
2766
    if (isset($config->lti_clientid)) {
2767
        $type->clientid = $config->lti_clientid;
2768
    }
2769
    if ((!empty($type->ltiversion) && $type->ltiversion === LTI_VERSION_1P3) && empty($type->clientid)) {
2770
        $type->clientid = registration_helper::get()->new_clientid();
2771
    } else if (empty($type->clientid)) {
2772
        $type->clientid = null;
2773
    }
2774
    if (isset($config->lti_coursevisible)) {
2775
        $type->coursevisible = $config->lti_coursevisible;
2776
    }
2777
 
2778
    if (isset($config->lti_icon)) {
2779
        $type->icon = $config->lti_icon;
2780
    }
2781
    if (isset($config->lti_secureicon)) {
2782
        $type->secureicon = $config->lti_secureicon;
2783
    }
2784
 
2785
    $type->forcessl = !empty($config->lti_forcessl) ? $config->lti_forcessl : 0;
2786
    $config->lti_forcessl = $type->forcessl;
2787
    if (isset($config->lti_contentitem)) {
2788
        $type->contentitem = !empty($config->lti_contentitem) ? $config->lti_contentitem : 0;
2789
        $config->lti_contentitem = $type->contentitem;
2790
    }
2791
    if (isset($config->lti_toolurl_ContentItemSelectionRequest)) {
2792
        if (!empty($config->lti_toolurl_ContentItemSelectionRequest)) {
2793
            $type->toolurl_ContentItemSelectionRequest = $config->lti_toolurl_ContentItemSelectionRequest;
2794
        } else {
2795
            $type->toolurl_ContentItemSelectionRequest = '';
2796
        }
2797
        $config->lti_toolurl_ContentItemSelectionRequest = $type->toolurl_ContentItemSelectionRequest;
2798
    }
2799
 
2800
    $type->timemodified = time();
2801
 
2802
    unset ($config->lti_typename);
2803
    unset ($config->lti_toolurl);
2804
    unset ($config->lti_description);
2805
    unset ($config->lti_ltiversion);
2806
    unset ($config->lti_clientid);
2807
    unset ($config->lti_icon);
2808
    unset ($config->lti_secureicon);
2809
}
2810
 
2811
function lti_update_type($type, $config) {
2812
    global $DB, $CFG;
2813
 
2814
    lti_prepare_type_for_save($type, $config);
2815
 
2816
    if (lti_request_is_using_ssl() && !empty($type->secureicon)) {
2817
        $clearcache = !isset($config->oldicon) || ($config->oldicon !== $type->secureicon);
2818
    } else {
2819
        $clearcache = isset($type->icon) && (!isset($config->oldicon) || ($config->oldicon !== $type->icon));
2820
    }
2821
    unset($config->oldicon);
2822
 
2823
    if ($DB->update_record('lti_types', $type)) {
2824
        foreach ($config as $key => $value) {
2825
            if (substr($key, 0, 4) == 'lti_' && !is_null($value)) {
2826
                $record = new \StdClass();
2827
                $record->typeid = $type->id;
2828
                $record->name = substr($key, 4);
2829
                $record->value = $value;
2830
                lti_update_config($record);
2831
            }
2832
            if (substr($key, 0, 11) == 'ltiservice_' && !is_null($value)) {
2833
                $record = new \StdClass();
2834
                $record->typeid = $type->id;
2835
                $record->name = $key;
2836
                $record->value = $value;
2837
                lti_update_config($record);
2838
            }
2839
        }
2840
        if (isset($type->toolproxyid) && $type->ltiversion === LTI_VERSION_1P3) {
2841
            // We need to remove the tool proxy for this tool to function under 1.3.
2842
            $toolproxyid = $type->toolproxyid;
2843
            $DB->delete_records('lti_tool_settings', array('toolproxyid' => $toolproxyid));
2844
            $DB->delete_records('lti_tool_proxies', array('id' => $toolproxyid));
2845
            $type->toolproxyid = null;
2846
            $DB->update_record('lti_types', $type);
2847
        }
2848
        $DB->delete_records('lti_types_categories', ['typeid' => $type->id]);
2849
        if (isset($config->lti_coursecategories) && !empty($config->lti_coursecategories)) {
2850
            lti_type_add_categories($type->id, $config->lti_coursecategories);
2851
        }
2852
        require_once($CFG->libdir.'/modinfolib.php');
2853
        if ($clearcache) {
2854
            $sql = "SELECT cm.id, cm.course
2855
                      FROM {course_modules} cm
2856
                      JOIN {modules} m ON cm.module = m.id
2857
                      JOIN {lti} l ON l.course = cm.course
2858
                     WHERE m.name = :name AND l.typeid = :typeid";
2859
 
2860
            $rs = $DB->get_recordset_sql($sql, ['name' => 'lti', 'typeid' => $type->id]);
2861
 
2862
            $courseids = [];
2863
            foreach ($rs as $record) {
2864
                $courseids[] = $record->course;
2865
                \course_modinfo::purge_course_module_cache($record->course, $record->id);
2866
            }
2867
            $rs->close();
2868
            $courseids = array_unique($courseids);
2869
            foreach ($courseids as $courseid) {
2870
                rebuild_course_cache($courseid, false, true);
2871
            }
2872
        }
2873
    }
2874
}
2875
 
2876
/**
2877
 * Add LTI Type course category.
2878
 *
2879
 * @param int $typeid
2880
 * @param string $lticoursecategories Comma separated list of course categories.
2881
 * @return void
2882
 */
2883
function lti_type_add_categories(int $typeid, string $lticoursecategories = ''): void {
2884
    global $DB;
2885
    $coursecategories = explode(',', $lticoursecategories);
2886
    foreach ($coursecategories as $coursecategory) {
2887
        $DB->insert_record('lti_types_categories', ['typeid' => $typeid, 'categoryid' => $coursecategory]);
2888
    }
2889
}
2890
 
2891
function lti_add_type($type, $config) {
2892
    global $USER, $SITE, $DB;
2893
 
2894
    lti_prepare_type_for_save($type, $config);
2895
 
2896
    if (!isset($type->state)) {
2897
        $type->state = LTI_TOOL_STATE_PENDING;
2898
    }
2899
 
2900
    if (!isset($type->ltiversion)) {
2901
        $type->ltiversion = LTI_VERSION_1;
2902
    }
2903
 
2904
    if (!isset($type->timecreated)) {
2905
        $type->timecreated = time();
2906
    }
2907
 
2908
    if (!isset($type->createdby)) {
2909
        $type->createdby = $USER->id;
2910
    }
2911
 
2912
    if (!isset($type->course)) {
2913
        $type->course = $SITE->id;
2914
    }
2915
 
2916
    // Create a salt value to be used for signing passed data to extension services
2917
    // The outcome service uses the service salt on the instance. This can be used
2918
    // for communication with services not related to a specific LTI instance.
2919
    $config->lti_servicesalt = uniqid('', true);
2920
 
2921
    $id = $DB->insert_record('lti_types', $type);
2922
 
2923
    if ($id) {
2924
        foreach ($config as $key => $value) {
2925
            if (!is_null($value)) {
2926
                if (substr($key, 0, 4) === 'lti_') {
2927
                    $fieldname = substr($key, 4);
2928
                } else if (substr($key, 0, 11) !== 'ltiservice_') {
2929
                    continue;
2930
                } else {
2931
                    $fieldname = $key;
2932
                }
2933
 
2934
                $record = new \StdClass();
2935
                $record->typeid = $id;
2936
                $record->name = $fieldname;
2937
                $record->value = $value;
2938
 
2939
                lti_add_config($record);
2940
            }
2941
        }
2942
        if (isset($config->lti_coursecategories) && !empty($config->lti_coursecategories)) {
2943
            lti_type_add_categories($id, $config->lti_coursecategories);
2944
        }
2945
    }
2946
 
2947
    return $id;
2948
}
2949
 
2950
/**
2951
 * Given an array of tool proxies, filter them based on their state
2952
 *
2953
 * @param array $toolproxies An array of lti_tool_proxies records
2954
 * @param int $state One of the LTI_TOOL_PROXY_STATE_* constants
2955
 *
2956
 * @return array
2957
 */
2958
function lti_filter_tool_proxy_types(array $toolproxies, $state) {
2959
    $return = array();
2960
    foreach ($toolproxies as $key => $toolproxy) {
2961
        if ($toolproxy->state == $state) {
2962
            $return[$key] = $toolproxy;
2963
        }
2964
    }
2965
    return $return;
2966
}
2967
 
2968
/**
2969
 * Get the tool proxy instance given its GUID
2970
 *
2971
 * @param string  $toolproxyguid   Tool proxy GUID value
2972
 *
2973
 * @return object
2974
 */
2975
function lti_get_tool_proxy_from_guid($toolproxyguid) {
2976
    global $DB;
2977
 
2978
    $toolproxy = $DB->get_record('lti_tool_proxies', array('guid' => $toolproxyguid));
2979
 
2980
    return $toolproxy;
2981
}
2982
 
2983
/**
2984
 * Get the tool proxy instance given its registration URL
2985
 *
2986
 * @param string $regurl Tool proxy registration URL
2987
 *
2988
 * @return array The record of the tool proxy with this url
2989
 */
2990
function lti_get_tool_proxies_from_registration_url($regurl) {
2991
    global $DB;
2992
 
2993
    return $DB->get_records_sql(
2994
        'SELECT * FROM {lti_tool_proxies}
2995
        WHERE '.$DB->sql_compare_text('regurl', 256).' = :regurl',
2996
        array('regurl' => $regurl)
2997
    );
2998
}
2999
 
3000
/**
3001
 * Generates some of the tool proxy configuration based on the admin configuration details
3002
 *
3003
 * @param int $id
3004
 *
3005
 * @return mixed Tool Proxy details
3006
 */
3007
function lti_get_tool_proxy($id) {
3008
    global $DB;
3009
 
3010
    $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $id));
3011
    return $toolproxy;
3012
}
3013
 
3014
/**
3015
 * Returns lti tool proxies.
3016
 *
3017
 * @param bool $orphanedonly Only retrieves tool proxies that have no type associated with them
3018
 * @return array of basicLTI types
3019
 */
3020
function lti_get_tool_proxies($orphanedonly) {
3021
    global $DB;
3022
 
3023
    if ($orphanedonly) {
3024
        $usedproxyids = array_values($DB->get_fieldset_select('lti_types', 'toolproxyid', 'toolproxyid IS NOT NULL'));
3025
        $proxies = $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC');
3026
        foreach ($proxies as $key => $value) {
3027
            if (in_array($value->id, $usedproxyids)) {
3028
                unset($proxies[$key]);
3029
            }
3030
        }
3031
        return $proxies;
3032
    } else {
3033
        return $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC');
3034
    }
3035
}
3036
 
3037
/**
3038
 * Generates some of the tool proxy configuration based on the admin configuration details
3039
 *
3040
 * @param int $id
3041
 *
3042
 * @return mixed  Tool Proxy details
3043
 */
3044
function lti_get_tool_proxy_config($id) {
3045
    $toolproxy = lti_get_tool_proxy($id);
3046
 
3047
    $tp = new \stdClass();
3048
    $tp->lti_registrationname = $toolproxy->name;
3049
    $tp->toolproxyid = $toolproxy->id;
3050
    $tp->state = $toolproxy->state;
3051
    $tp->lti_registrationurl = $toolproxy->regurl;
3052
    $tp->lti_capabilities = explode("\n", $toolproxy->capabilityoffered);
3053
    $tp->lti_services = explode("\n", $toolproxy->serviceoffered);
3054
 
3055
    return $tp;
3056
}
3057
 
3058
/**
3059
 * Update the database with a tool proxy instance
3060
 *
3061
 * @param object   $config    Tool proxy definition
3062
 *
3063
 * @return int  Record id number
3064
 */
3065
function lti_add_tool_proxy($config) {
3066
    global $USER, $DB;
3067
 
3068
    $toolproxy = new \stdClass();
3069
    if (isset($config->lti_registrationname)) {
3070
        $toolproxy->name = trim($config->lti_registrationname);
3071
    }
3072
    if (isset($config->lti_registrationurl)) {
3073
        $toolproxy->regurl = trim($config->lti_registrationurl);
3074
    }
3075
    if (isset($config->lti_capabilities)) {
3076
        $toolproxy->capabilityoffered = implode("\n", $config->lti_capabilities);
3077
    } else {
3078
        $toolproxy->capabilityoffered = implode("\n", array_keys(lti_get_capabilities()));
3079
    }
3080
    if (isset($config->lti_services)) {
3081
        $toolproxy->serviceoffered = implode("\n", $config->lti_services);
3082
    } else {
3083
        $func = function($s) {
3084
            return $s->get_id();
3085
        };
3086
        $servicenames = array_map($func, lti_get_services());
3087
        $toolproxy->serviceoffered = implode("\n", $servicenames);
3088
    }
3089
    if (isset($config->toolproxyid) && !empty($config->toolproxyid)) {
3090
        $toolproxy->id = $config->toolproxyid;
3091
        if (!isset($toolproxy->state) || ($toolproxy->state != LTI_TOOL_PROXY_STATE_ACCEPTED)) {
3092
            $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED;
3093
            $toolproxy->guid = random_string();
3094
            $toolproxy->secret = random_string();
3095
        }
3096
        $id = lti_update_tool_proxy($toolproxy);
3097
    } else {
3098
        $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED;
3099
        $toolproxy->timemodified = time();
3100
        $toolproxy->timecreated = $toolproxy->timemodified;
3101
        if (!isset($toolproxy->createdby)) {
3102
            $toolproxy->createdby = $USER->id;
3103
        }
3104
        $toolproxy->guid = random_string();
3105
        $toolproxy->secret = random_string();
3106
        $id = $DB->insert_record('lti_tool_proxies', $toolproxy);
3107
    }
3108
 
3109
    return $id;
3110
}
3111
 
3112
/**
3113
 * Updates a tool proxy in the database
3114
 *
3115
 * @param object  $toolproxy   Tool proxy
3116
 *
3117
 * @return int    Record id number
3118
 */
3119
function lti_update_tool_proxy($toolproxy) {
3120
    global $DB;
3121
 
3122
    $toolproxy->timemodified = time();
3123
    $id = $DB->update_record('lti_tool_proxies', $toolproxy);
3124
 
3125
    return $id;
3126
}
3127
 
3128
/**
3129
 * Delete a Tool Proxy
3130
 *
3131
 * @param int $id   Tool Proxy id
3132
 */
3133
function lti_delete_tool_proxy($id) {
3134
    global $DB;
3135
    $DB->delete_records('lti_tool_settings', array('toolproxyid' => $id));
3136
    $tools = $DB->get_records('lti_types', array('toolproxyid' => $id));
3137
    foreach ($tools as $tool) {
3138
        lti_delete_type($tool->id);
3139
    }
3140
    $DB->delete_records('lti_tool_proxies', array('id' => $id));
3141
}
3142
 
3143
/**
3144
 * Get both LTI tool proxies and tool types.
3145
 *
3146
 * If limit and offset are not zero, a subset of the tools will be returned. Tool proxies will be counted before tool
3147
 * types.
3148
 * For example: If 10 tool proxies and 10 tool types exist, and the limit is set to 15, then 10 proxies and 5 types
3149
 * will be returned.
3150
 *
3151
 * @param int $limit Maximum number of tools returned.
3152
 * @param int $offset Do not return tools before offset index.
3153
 * @param bool $orphanedonly If true, only return orphaned proxies.
3154
 * @param int $toolproxyid If not 0, only return tool types that have this tool proxy id.
3155
 * @return array list(proxies[], types[]) List containing array of tool proxies and array of tool types.
3156
 */
3157
function lti_get_lti_types_and_proxies(int $limit = 0, int $offset = 0, bool $orphanedonly = false, int $toolproxyid = 0): array {
3158
    global $DB;
3159
 
3160
    if ($orphanedonly) {
3161
        $orphanedproxiessql = helper::get_tool_proxy_sql($orphanedonly, false);
3162
        $countsql = helper::get_tool_proxy_sql($orphanedonly, true);
3163
        $proxies  = $DB->get_records_sql($orphanedproxiessql, null, $offset, $limit);
3164
        $totalproxiescount = $DB->count_records_sql($countsql);
3165
    } else {
3166
        $proxies = $DB->get_records('lti_tool_proxies', null, 'name ASC, state DESC, timemodified DESC',
3167
            '*', $offset, $limit);
3168
        $totalproxiescount = $DB->count_records('lti_tool_proxies');
3169
    }
3170
 
3171
    // Find new offset and limit for tool types after getting proxies and set up query.
3172
    $typesoffset = max($offset - $totalproxiescount, 0); // Set to 0 if negative.
3173
    $typeslimit = max($limit - count($proxies), 0); // Set to 0 if negative.
3174
    $typesparams = [];
3175
    if (!empty($toolproxyid)) {
3176
        $typesparams['toolproxyid'] = $toolproxyid;
3177
    }
3178
 
3179
    $types = $DB->get_records('lti_types', $typesparams, 'name ASC, state DESC, timemodified DESC',
3180
            '*', $typesoffset, $typeslimit);
3181
 
3182
    return [$proxies, array_map('serialise_tool_type', $types)];
3183
}
3184
 
3185
/**
3186
 * Get the total number of LTI tool types and tool proxies.
3187
 *
3188
 * @param bool $orphanedonly If true, only count orphaned proxies.
3189
 * @param int $toolproxyid If not 0, only count tool types that have this tool proxy id.
3190
 * @return int Count of tools.
3191
 */
3192
function lti_get_lti_types_and_proxies_count(bool $orphanedonly = false, int $toolproxyid = 0): int {
3193
    global $DB;
3194
 
3195
    $typessql = "SELECT count(*)
3196
                   FROM {lti_types}";
3197
    $typesparams = [];
3198
    if (!empty($toolproxyid)) {
3199
        $typessql .= " WHERE toolproxyid = :toolproxyid";
3200
        $typesparams['toolproxyid'] = $toolproxyid;
3201
    }
3202
 
3203
    $proxiessql = helper::get_tool_proxy_sql($orphanedonly, true);
3204
 
3205
    $countsql = "SELECT ($typessql) + ($proxiessql) as total" . $DB->sql_null_from_clause();
3206
 
3207
    return $DB->count_records_sql($countsql, $typesparams);
3208
}
3209
 
3210
/**
3211
 * Add a tool configuration in the database
3212
 *
3213
 * @param object $config   Tool configuration
3214
 *
3215
 * @return int Record id number
3216
 */
3217
function lti_add_config($config) {
3218
    global $DB;
3219
 
3220
    return $DB->insert_record('lti_types_config', $config);
3221
}
3222
 
3223
/**
3224
 * Updates a tool configuration in the database
3225
 *
3226
 * @param object  $config   Tool configuration
3227
 *
3228
 * @return mixed Record id number
3229
 */
3230
function lti_update_config($config) {
3231
    global $DB;
3232
 
3233
    $old = $DB->get_record('lti_types_config', array('typeid' => $config->typeid, 'name' => $config->name));
3234
 
3235
    if ($old) {
3236
        $config->id = $old->id;
3237
        $return = $DB->update_record('lti_types_config', $config);
3238
    } else {
3239
        $return = $DB->insert_record('lti_types_config', $config);
3240
    }
3241
    return $return;
3242
}
3243
 
3244
/**
3245
 * Gets the tool settings
3246
 *
3247
 * @param int  $toolproxyid   Id of tool proxy record (or tool ID if negative)
3248
 * @param int  $courseid      Id of course (null if system settings)
3249
 * @param int  $instanceid    Id of course module (null if system or context settings)
3250
 *
3251
 * @return array  Array settings
3252
 */
3253
function lti_get_tool_settings($toolproxyid, $courseid = null, $instanceid = null) {
3254
    global $DB;
3255
 
3256
    $settings = array();
3257
    if ($toolproxyid > 0) {
3258
        $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('toolproxyid' => $toolproxyid,
3259
            'course' => $courseid, 'coursemoduleid' => $instanceid));
3260
    } else {
3261
        $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('typeid' => -$toolproxyid,
3262
            'course' => $courseid, 'coursemoduleid' => $instanceid));
3263
    }
3264
    if ($settingsstr !== false) {
3265
        $settings = json_decode($settingsstr, true);
3266
    }
3267
    return $settings;
3268
}
3269
 
3270
/**
3271
 * Sets the tool settings (
3272
 *
3273
 * @param array  $settings      Array of settings
3274
 * @param int    $toolproxyid   Id of tool proxy record (or tool ID if negative)
3275
 * @param int    $courseid      Id of course (null if system settings)
3276
 * @param int    $instanceid    Id of course module (null if system or context settings)
3277
 */
3278
function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $instanceid = null) {
3279
    global $DB;
3280
 
3281
    $json = json_encode($settings);
3282
    if ($toolproxyid >= 0) {
3283
        $record = $DB->get_record('lti_tool_settings', array('toolproxyid' => $toolproxyid,
3284
            'course' => $courseid, 'coursemoduleid' => $instanceid));
3285
    } else {
3286
        $record = $DB->get_record('lti_tool_settings', array('typeid' => -$toolproxyid,
3287
            'course' => $courseid, 'coursemoduleid' => $instanceid));
3288
    }
3289
    if ($record !== false) {
3290
        $DB->update_record('lti_tool_settings', (object)array('id' => $record->id, 'settings' => $json, 'timemodified' => time()));
3291
    } else {
3292
        $record = new \stdClass();
3293
        if ($toolproxyid > 0) {
3294
            $record->toolproxyid = $toolproxyid;
3295
        } else {
3296
            $record->typeid = -$toolproxyid;
3297
        }
3298
        $record->course = $courseid;
3299
        $record->coursemoduleid = $instanceid;
3300
        $record->settings = $json;
3301
        $record->timecreated = time();
3302
        $record->timemodified = $record->timecreated;
3303
        $DB->insert_record('lti_tool_settings', $record);
3304
    }
3305
}
3306
 
3307
/**
3308
 * Signs the petition to launch the external tool using OAuth
3309
 *
3310
 * @param array  $oldparms     Parameters to be passed for signing
3311
 * @param string $endpoint     url of the external tool
3312
 * @param string $method       Method for sending the parameters (e.g. POST)
3313
 * @param string $oauthconsumerkey
3314
 * @param string $oauthconsumersecret
3315
 * @return array|null
3316
 */
3317
function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret) {
3318
 
3319
    $parms = $oldparms;
3320
 
3321
    $testtoken = '';
3322
 
3323
    // TODO: Switch to core oauthlib once implemented - MDL-30149.
3324
    $hmacmethod = new lti\OAuthSignatureMethod_HMAC_SHA1();
3325
    $testconsumer = new lti\OAuthConsumer($oauthconsumerkey, $oauthconsumersecret, null);
3326
    $accreq = lti\OAuthRequest::from_consumer_and_token($testconsumer, $testtoken, $method, $endpoint, $parms);
3327
    $accreq->sign_request($hmacmethod, $testconsumer, $testtoken);
3328
 
3329
    $newparms = $accreq->get_parameters();
3330
 
3331
    return $newparms;
3332
}
3333
 
3334
/**
3335
 * Converts the message paramters to their equivalent JWT claim and signs the payload to launch the external tool using JWT
3336
 *
3337
 * @param array  $parms        Parameters to be passed for signing
3338
 * @param string $endpoint     url of the external tool
3339
 * @param string $oauthconsumerkey
3340
 * @param string $typeid       ID of LTI tool type
3341
 * @param string $nonce        Nonce value to use
3342
 * @return array|null
3343
 */
3344
function lti_sign_jwt($parms, $endpoint, $oauthconsumerkey, $typeid = 0, $nonce = '') {
3345
    global $CFG;
3346
 
3347
    if (empty($typeid)) {
3348
        $typeid = 0;
3349
    }
3350
    $messagetypemapping = lti_get_jwt_message_type_mapping();
3351
    if (isset($parms['lti_message_type']) && array_key_exists($parms['lti_message_type'], $messagetypemapping)) {
3352
        $parms['lti_message_type'] = $messagetypemapping[$parms['lti_message_type']];
3353
    }
3354
    if (isset($parms['roles'])) {
3355
        $roles = explode(',', $parms['roles']);
3356
        $newroles = array();
3357
        foreach ($roles as $role) {
3358
            if (strpos($role, 'urn:lti:role:ims/lis/') === 0) {
3359
                $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#' . substr($role, 21);
3360
            } else if (strpos($role, 'urn:lti:instrole:ims/lis/') === 0) {
3361
                $role = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#' . substr($role, 25);
3362
            } else if (strpos($role, 'urn:lti:sysrole:ims/lis/') === 0) {
3363
                $role = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#' . substr($role, 24);
3364
            } else if ((strpos($role, '://') === false) && (strpos($role, 'urn:') !== 0)) {
3365
                $role = "http://purl.imsglobal.org/vocab/lis/v2/membership#{$role}";
3366
            }
3367
            $newroles[] = $role;
3368
        }
3369
        $parms['roles'] = implode(',', $newroles);
3370
    }
3371
 
3372
    $now = time();
3373
    if (empty($nonce)) {
3374
        $nonce = bin2hex(openssl_random_pseudo_bytes(10));
3375
    }
3376
    $claimmapping = lti_get_jwt_claim_mapping();
3377
    $payload = array(
3378
        'nonce' => $nonce,
3379
        'iat' => $now,
3380
        'exp' => $now + 60,
3381
    );
3382
    $payload['iss'] = $CFG->wwwroot;
3383
    $payload['aud'] = $oauthconsumerkey;
3384
    $payload[LTI_JWT_CLAIM_PREFIX . '/claim/deployment_id'] = strval($typeid);
3385
    $payload[LTI_JWT_CLAIM_PREFIX . '/claim/target_link_uri'] = $endpoint;
3386
 
3387
    foreach ($parms as $key => $value) {
3388
        $claim = LTI_JWT_CLAIM_PREFIX;
3389
        if (array_key_exists($key, $claimmapping)) {
3390
            $mapping = $claimmapping[$key];
3391
            $type = $mapping["type"] ?? "string";
3392
            if ($mapping['isarray']) {
3393
                $value = explode(',', $value);
3394
                sort($value);
3395
            } else if ($type == 'boolean') {
3396
                $value = isset($value) && ($value == 'true');
3397
            }
3398
            if (!empty($mapping['suffix'])) {
3399
                $claim .= "-{$mapping['suffix']}";
3400
            }
3401
            $claim .= '/claim/';
3402
            if (is_null($mapping['group'])) {
3403
                $payload[$mapping['claim']] = $value;
3404
            } else if (empty($mapping['group'])) {
3405
                $payload["{$claim}{$mapping['claim']}"] = $value;
3406
            } else {
3407
                $claim .= $mapping['group'];
3408
                $payload[$claim][$mapping['claim']] = $value;
3409
            }
3410
        } else if (strpos($key, 'custom_') === 0) {
3411
            $payload["{$claim}/claim/custom"][substr($key, 7)] = $value;
3412
        } else if (strpos($key, 'ext_') === 0) {
3413
            $payload["{$claim}/claim/ext"][substr($key, 4)] = $value;
3414
        }
3415
    }
3416
 
3417
    $privatekey = jwks_helper::get_private_key();
3418
    $jwt = JWT::encode($payload, $privatekey['key'], 'RS256', $privatekey['kid']);
3419
 
3420
    $newparms = array();
3421
    $newparms['id_token'] = $jwt;
3422
 
3423
    return $newparms;
3424
}
3425
 
3426
/**
3427
 * Verfies the JWT and converts its claims to their equivalent message parameter.
3428
 *
3429
 * @param int    $typeid
3430
 * @param string $jwtparam   JWT parameter
3431
 *
3432
 * @return array  message parameters
3433
 * @throws moodle_exception
3434
 */
3435
function lti_convert_from_jwt($typeid, $jwtparam) {
3436
 
3437
    $params = array();
3438
    $parts = explode('.', $jwtparam);
3439
    $ok = (count($parts) === 3);
3440
    if ($ok) {
3441
        $payload = JWT::urlsafeB64Decode($parts[1]);
3442
        $claims = json_decode($payload, true);
3443
        $ok = !is_null($claims) && !empty($claims['iss']);
3444
    }
3445
    if ($ok) {
3446
        lti_verify_jwt_signature($typeid, $claims['iss'], $jwtparam);
3447
        $params['oauth_consumer_key'] = $claims['iss'];
3448
        foreach (lti_get_jwt_claim_mapping() as $key => $mapping) {
3449
            $claim = LTI_JWT_CLAIM_PREFIX;
3450
            if (!empty($mapping['suffix'])) {
3451
                $claim .= "-{$mapping['suffix']}";
3452
            }
3453
            $claim .= '/claim/';
3454
            if (is_null($mapping['group'])) {
3455
                $claim = $mapping['claim'];
3456
            } else if (empty($mapping['group'])) {
3457
                $claim .= $mapping['claim'];
3458
            } else {
3459
                $claim .= $mapping['group'];
3460
            }
3461
            if (isset($claims[$claim])) {
3462
                $value = null;
3463
                if (empty($mapping['group'])) {
3464
                    $value = $claims[$claim];
3465
                } else {
3466
                    $group = $claims[$claim];
3467
                    if (is_array($group) && array_key_exists($mapping['claim'], $group)) {
3468
                        $value = $group[$mapping['claim']];
3469
                    }
3470
                }
3471
                if (!empty($value) && $mapping['isarray']) {
3472
                    if (is_array($value)) {
3473
                        if (is_array($value[0])) {
3474
                            $value = json_encode($value);
3475
                        } else {
3476
                            $value = implode(',', $value);
3477
                        }
3478
                    }
3479
                }
3480
                if (!is_null($value) && is_string($value) && (strlen($value) > 0)) {
3481
                    $params[$key] = $value;
3482
                }
3483
            }
3484
            $claim = LTI_JWT_CLAIM_PREFIX . '/claim/custom';
3485
            if (isset($claims[$claim])) {
3486
                $custom = $claims[$claim];
3487
                if (is_array($custom)) {
3488
                    foreach ($custom as $key => $value) {
3489
                        $params["custom_{$key}"] = $value;
3490
                    }
3491
                }
3492
            }
3493
            $claim = LTI_JWT_CLAIM_PREFIX . '/claim/ext';
3494
            if (isset($claims[$claim])) {
3495
                $ext = $claims[$claim];
3496
                if (is_array($ext)) {
3497
                    foreach ($ext as $key => $value) {
3498
                        $params["ext_{$key}"] = $value;
3499
                    }
3500
                }
3501
            }
3502
        }
3503
    }
3504
    if (isset($params['content_items'])) {
3505
        $params['content_items'] = lti_convert_content_items($params['content_items']);
3506
    }
3507
    $messagetypemapping = lti_get_jwt_message_type_mapping();
3508
    if (isset($params['lti_message_type']) && array_key_exists($params['lti_message_type'], $messagetypemapping)) {
3509
        $params['lti_message_type'] = $messagetypemapping[$params['lti_message_type']];
3510
    }
3511
    return $params;
3512
}
3513
 
3514
/**
3515
 * Posts the launch petition HTML
3516
 *
3517
 * @param array $newparms   Signed parameters
3518
 * @param string $endpoint  URL of the external tool
3519
 * @param bool $debug       Debug (true/false)
3520
 * @return string
3521
 */
3522
function lti_post_launch_html($newparms, $endpoint, $debug=false) {
3523
    $r = "<form action=\"" . $endpoint .
3524
        "\" name=\"ltiLaunchForm\" id=\"ltiLaunchForm\" method=\"post\" encType=\"application/x-www-form-urlencoded\">\n";
3525
 
3526
    // Contruct html for the launch parameters.
3527
    foreach ($newparms as $key => $value) {
3528
        $key = htmlspecialchars($key, ENT_COMPAT);
3529
        $value = htmlspecialchars($value, ENT_COMPAT);
3530
        if ( $key == "ext_submit" ) {
3531
            $r .= "<input type=\"submit\"";
3532
        } else {
3533
            $r .= "<input type=\"hidden\" name=\"{$key}\"";
3534
        }
3535
        $r .= " value=\"";
3536
        $r .= $value;
3537
        $r .= "\"/>\n";
3538
    }
3539
 
3540
    if ( $debug ) {
3541
        $r .= "<script language=\"javascript\"> \n";
3542
        $r .= "  //<![CDATA[ \n";
3543
        $r .= "function basicltiDebugToggle() {\n";
3544
        $r .= "    var ele = document.getElementById(\"basicltiDebug\");\n";
3545
        $r .= "    if (ele.style.display == \"block\") {\n";
3546
        $r .= "        ele.style.display = \"none\";\n";
3547
        $r .= "    }\n";
3548
        $r .= "    else {\n";
3549
        $r .= "        ele.style.display = \"block\";\n";
3550
        $r .= "    }\n";
3551
        $r .= "} \n";
3552
        $r .= "  //]]> \n";
3553
        $r .= "</script>\n";
3554
        $r .= "<a id=\"displayText\" href=\"javascript:basicltiDebugToggle();\">";
3555
        $r .= get_string("toggle_debug_data", "lti")."</a>\n";
3556
        $r .= "<div id=\"basicltiDebug\" style=\"display:none\">\n";
3557
        $r .= "<b>".get_string("basiclti_endpoint", "lti")."</b><br/>\n";
3558
        $r .= $endpoint . "<br/>\n&nbsp;<br/>\n";
3559
        $r .= "<b>".get_string("basiclti_parameters", "lti")."</b><br/>\n";
3560
        foreach ($newparms as $key => $value) {
3561
            $key = htmlspecialchars($key, ENT_COMPAT);
3562
            $value = htmlspecialchars($value, ENT_COMPAT);
3563
            $r .= "$key = $value<br/>\n";
3564
        }
3565
        $r .= "&nbsp;<br/>\n";
3566
        $r .= "</div>\n";
3567
    }
3568
    $r .= "</form>\n";
3569
 
3570
    // Auto-submit the form if endpoint is set.
3571
    if ($endpoint !== '' && !$debug) {
3572
        $r .= " <script type=\"text/javascript\"> \n" .
3573
            "  //<![CDATA[ \n" .
3574
            "    document.ltiLaunchForm.submit(); \n" .
3575
            "  //]]> \n" .
3576
            " </script> \n";
3577
    }
3578
    return $r;
3579
}
3580
 
3581
/**
3582
 * Generate the form for initiating a login request for an LTI 1.3 message
3583
 *
3584
 * @param int            $courseid  Course ID
3585
 * @param int            $cmid        LTI instance ID
3586
 * @param stdClass|null  $instance  LTI instance
3587
 * @param stdClass       $config    Tool type configuration
3588
 * @param string         $messagetype   LTI message type
3589
 * @param string         $title     Title of content item
3590
 * @param string         $text      Description of content item
3591
 * @param int            $foruserid Id of the user targeted by the launch
3592
 * @return string
3593
 */
3594
function lti_initiate_login($courseid, $cmid, $instance, $config, $messagetype = 'basic-lti-launch-request',
3595
        $title = '', $text = '', $foruserid = 0) {
3596
    global $SESSION;
3597
 
3598
    $params = lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid, $title, $text);
3599
 
3600
    $r = "<form action=\"" . $config->lti_initiatelogin .
3601
        "\" name=\"ltiInitiateLoginForm\" id=\"ltiInitiateLoginForm\" method=\"post\" " .
3602
        "encType=\"application/x-www-form-urlencoded\">\n";
3603
 
3604
    foreach ($params as $key => $value) {
3605
        $key = htmlspecialchars($key, ENT_COMPAT);
3606
        $value = htmlspecialchars($value, ENT_COMPAT);
3607
        $r .= "  <input type=\"hidden\" name=\"{$key}\" value=\"{$value}\"/>\n";
3608
    }
3609
    $r .= "</form>\n";
3610
 
3611
    $r .= "<script type=\"text/javascript\">\n" .
3612
        "//<![CDATA[\n" .
3613
        "document.ltiInitiateLoginForm.submit();\n" .
3614
        "//]]>\n" .
3615
        "</script>\n";
3616
 
3617
    return $r;
3618
}
3619
 
3620
/**
3621
 * Prepares an LTI 1.3 login request
3622
 *
3623
 * @param int            $courseid  Course ID
3624
 * @param int            $cmid        Course Module instance ID
3625
 * @param stdClass|null  $instance  LTI instance
3626
 * @param stdClass       $config    Tool type configuration
3627
 * @param string         $messagetype   LTI message type
3628
 * @param int            $foruserid Id of the user targeted by the launch
3629
 * @param string         $title     Title of content item
3630
 * @param string         $text      Description of content item
3631
 * @return array Login request parameters
3632
 */
3633
function lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid=0, $title = '', $text = '') {
3634
    global $USER, $CFG, $SESSION;
3635
    $ltihint = [];
3636
    if (!empty($instance)) {
3637
        $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $config->lti_toolurl;
3638
        $launchid = 'ltilaunch'.$instance->id.'_'.rand();
3639
        $ltihint['cmid'] = $cmid;
3640
        $SESSION->$launchid = "{$courseid},{$config->typeid},{$cmid},{$messagetype},{$foruserid},,";
3641
    } else {
3642
        $endpoint = $config->lti_toolurl;
3643
        if (($messagetype === 'ContentItemSelectionRequest') && !empty($config->lti_toolurl_ContentItemSelectionRequest)) {
3644
            $endpoint = $config->lti_toolurl_ContentItemSelectionRequest;
3645
        }
3646
        $launchid = "ltilaunch_$messagetype".rand();
3647
        $SESSION->$launchid =
3648
            "{$courseid},{$config->typeid},,{$messagetype},{$foruserid}," . base64_encode($title) . ',' . base64_encode($text);
3649
    }
3650
    $endpoint = trim($endpoint);
3651
    $services = lti_get_services();
3652
    foreach ($services as $service) {
3653
        [$endpoint] = $service->override_endpoint($messagetype ?? 'basic-lti-launch-request', $endpoint, '', $courseid, $instance);
3654
    }
3655
 
3656
    $ltihint['launchid'] = $launchid;
3657
    // If SSL is forced make sure https is on the normal launch URL.
3658
    if (isset($config->lti_forcessl) && ($config->lti_forcessl == '1')) {
3659
        $endpoint = lti_ensure_url_is_https($endpoint);
3660
    } else if (!strstr($endpoint, '://')) {
3661
        $endpoint = 'http://' . $endpoint;
3662
    }
3663
 
3664
    $params = array();
3665
    $params['iss'] = $CFG->wwwroot;
3666
    $params['target_link_uri'] = $endpoint;
3667
    $params['login_hint'] = $USER->id;
3668
    $params['lti_message_hint'] = json_encode($ltihint);
3669
    $params['client_id'] = $config->lti_clientid;
3670
    $params['lti_deployment_id'] = $config->typeid;
3671
    return $params;
3672
}
3673
 
3674
function lti_get_type($typeid) {
3675
    global $DB;
3676
 
3677
    return $DB->get_record('lti_types', array('id' => $typeid));
3678
}
3679
 
3680
function lti_get_launch_container($lti, $toolconfig) {
3681
    if (empty($lti->launchcontainer)) {
3682
        $lti->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
3683
    }
3684
 
3685
    if ($lti->launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) {
3686
        if (isset($toolconfig['launchcontainer'])) {
3687
            $launchcontainer = $toolconfig['launchcontainer'];
3688
        }
3689
    } else {
3690
        $launchcontainer = $lti->launchcontainer;
3691
    }
3692
 
3693
    if (empty($launchcontainer) || $launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) {
3694
        $launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
3695
    }
3696
 
3697
    $devicetype = core_useragent::get_device_type();
3698
 
3699
    // Scrolling within the object element doesn't work on iOS or Android
3700
    // Opening the popup window also had some issues in testing
3701
    // For mobile devices, always take up the entire screen to ensure the best experience.
3702
    if ($devicetype === core_useragent::DEVICETYPE_MOBILE || $devicetype === core_useragent::DEVICETYPE_TABLET ) {
3703
        $launchcontainer = LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW;
3704
    }
3705
 
3706
    return $launchcontainer;
3707
}
3708
 
3709
function lti_request_is_using_ssl() {
3710
    global $CFG;
3711
    return (stripos($CFG->wwwroot, 'https://') === 0);
3712
}
3713
 
3714
function lti_ensure_url_is_https($url) {
3715
    if (!strstr($url, '://')) {
3716
        $url = 'https://' . $url;
3717
    } else {
3718
        // If the URL starts with http, replace with https.
3719
        if (stripos($url, 'http://') === 0) {
3720
            $url = 'https://' . substr($url, 7);
3721
        }
3722
    }
3723
 
3724
    return $url;
3725
}
3726
 
3727
/**
3728
 * Determines if we should try to log the request
3729
 *
3730
 * @param string $rawbody
3731
 * @return bool
3732
 */
3733
function lti_should_log_request($rawbody) {
3734
    global $CFG;
3735
 
3736
    if (empty($CFG->mod_lti_log_users)) {
3737
        return false;
3738
    }
3739
 
3740
    $logusers = explode(',', $CFG->mod_lti_log_users);
3741
    if (empty($logusers)) {
3742
        return false;
3743
    }
3744
 
3745
    try {
3746
        $xml = new \SimpleXMLElement($rawbody);
3747
        $ns  = $xml->getNamespaces();
3748
        $ns  = array_shift($ns);
3749
        $xml->registerXPathNamespace('lti', $ns);
3750
        $requestuserid = '';
3751
        if ($node = $xml->xpath('//lti:userId')) {
3752
            $node = $node[0];
3753
            $requestuserid = clean_param((string) $node, PARAM_INT);
3754
        } else if ($node = $xml->xpath('//lti:sourcedId')) {
3755
            $node = $node[0];
3756
            $resultjson = json_decode((string) $node);
3757
            $requestuserid = clean_param($resultjson->data->userid, PARAM_INT);
3758
        }
3759
    } catch (Exception $e) {
3760
        return false;
3761
    }
3762
 
3763
    if (empty($requestuserid) or !in_array($requestuserid, $logusers)) {
3764
        return false;
3765
    }
3766
 
3767
    return true;
3768
}
3769
 
3770
/**
3771
 * Logs the request to a file in temp dir.
3772
 *
3773
 * @param string $rawbody
3774
 */
3775
function lti_log_request($rawbody) {
3776
    if ($tempdir = make_temp_directory('mod_lti', false)) {
3777
        if ($tempfile = tempnam($tempdir, 'mod_lti_request'.date('YmdHis'))) {
3778
            $content  = "Request Headers:\n";
3779
            foreach (moodle\mod\lti\OAuthUtil::get_headers() as $header => $value) {
3780
                $content .= "$header: $value\n";
3781
            }
3782
            $content .= "Request Body:\n";
3783
            $content .= $rawbody;
3784
 
3785
            file_put_contents($tempfile, $content);
3786
            chmod($tempfile, 0644);
3787
        }
3788
    }
3789
}
3790
 
3791
/**
3792
 * Log an LTI response.
3793
 *
3794
 * @param string $responsexml The response XML
3795
 * @param Exception $e If there was an exception, pass that too
3796
 */
3797
function lti_log_response($responsexml, $e = null) {
3798
    if ($tempdir = make_temp_directory('mod_lti', false)) {
3799
        if ($tempfile = tempnam($tempdir, 'mod_lti_response'.date('YmdHis'))) {
3800
            $content = '';
3801
            if ($e instanceof Exception) {
3802
                $info = get_exception_info($e);
3803
 
3804
                $content .= "Exception:\n";
3805
                $content .= "Message: $info->message\n";
3806
                $content .= "Debug info: $info->debuginfo\n";
3807
                $content .= "Backtrace:\n";
3808
                $content .= format_backtrace($info->backtrace, true);
3809
                $content .= "\n";
3810
            }
3811
            $content .= "Response XML:\n";
3812
            $content .= $responsexml;
3813
 
3814
            file_put_contents($tempfile, $content);
3815
            chmod($tempfile, 0644);
3816
        }
3817
    }
3818
}
3819
 
3820
/**
3821
 * Fetches LTI type configuration for an LTI instance
3822
 *
3823
 * @param stdClass $instance
3824
 * @return array Can be empty if no type is found
3825
 */
3826
function lti_get_type_config_by_instance($instance) {
3827
    $typeid = null;
3828
    if (empty($instance->typeid)) {
3829
        $tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course);
3830
        if ($tool) {
3831
            $typeid = $tool->id;
3832
        }
3833
    } else {
3834
        $typeid = $instance->typeid;
3835
    }
3836
    if (!empty($typeid)) {
3837
        return lti_get_type_config($typeid);
3838
    }
3839
    return array();
3840
}
3841
 
3842
/**
3843
 * Enforce type config settings onto the LTI instance
3844
 *
3845
 * @param stdClass $instance
3846
 * @param array $typeconfig
3847
 */
3848
function lti_force_type_config_settings($instance, array $typeconfig) {
3849
    $forced = array(
3850
        'instructorchoicesendname'      => 'sendname',
3851
        'instructorchoicesendemailaddr' => 'sendemailaddr',
3852
        'instructorchoiceacceptgrades'  => 'acceptgrades',
3853
    );
3854
 
3855
    foreach ($forced as $instanceparam => $typeconfigparam) {
3856
        if (array_key_exists($typeconfigparam, $typeconfig) && $typeconfig[$typeconfigparam] != LTI_SETTING_DELEGATE) {
3857
            $instance->$instanceparam = $typeconfig[$typeconfigparam];
3858
        }
3859
    }
3860
}
3861
 
3862
/**
3863
 * Initializes an array with the capabilities supported by the LTI module
3864
 *
3865
 * @return array List of capability names (without a dollar sign prefix)
3866
 */
3867
function lti_get_capabilities() {
3868
 
3869
    $capabilities = array(
3870
       'basic-lti-launch-request' => '',
3871
       'ContentItemSelectionRequest' => '',
3872
       'ToolProxyRegistrationRequest' => '',
3873
       'Context.id' => 'context_id',
3874
       'Context.title' => 'context_title',
3875
       'Context.label' => 'context_label',
3876
       'Context.id.history' => null,
3877
       'Context.sourcedId' => 'lis_course_section_sourcedid',
3878
       'Context.longDescription' => '$COURSE->summary',
3879
       'Context.timeFrame.begin' => '$COURSE->startdate',
3880
       'CourseSection.title' => 'context_title',
3881
       'CourseSection.label' => 'context_label',
3882
       'CourseSection.sourcedId' => 'lis_course_section_sourcedid',
3883
       'CourseSection.longDescription' => '$COURSE->summary',
3884
       'CourseSection.timeFrame.begin' => null,
3885
       'CourseSection.timeFrame.end' => null,
3886
       'ResourceLink.id' => 'resource_link_id',
3887
       'ResourceLink.title' => 'resource_link_title',
3888
       'ResourceLink.description' => 'resource_link_description',
3889
       'User.id' => 'user_id',
3890
       'User.username' => '$USER->username',
3891
       'Person.name.full' => 'lis_person_name_full',
3892
       'Person.name.given' => 'lis_person_name_given',
3893
       'Person.name.family' => 'lis_person_name_family',
3894
       'Person.email.primary' => 'lis_person_contact_email_primary',
3895
       'Person.sourcedId' => 'lis_person_sourcedid',
3896
       'Person.name.middle' => '$USER->middlename',
3897
       'Person.address.street1' => '$USER->address',
3898
       'Person.address.locality' => '$USER->city',
3899
       'Person.address.country' => '$USER->country',
3900
       'Person.address.timezone' => '$USER->timezone',
3901
       'Person.phone.primary' => '$USER->phone1',
3902
       'Person.phone.mobile' => '$USER->phone2',
3903
       'Person.webaddress' => '$USER->url',
3904
       'Membership.role' => 'roles',
3905
       'Result.sourcedId' => 'lis_result_sourcedid',
3906
       'Result.autocreate' => 'lis_outcome_service_url',
3907
       'BasicOutcome.sourcedId' => 'lis_result_sourcedid',
3908
       'BasicOutcome.url' => 'lis_outcome_service_url',
3909
       'Moodle.Person.userGroupIds' => null);
3910
 
3911
    return $capabilities;
3912
 
3913
}
3914
 
3915
/**
3916
 * Initializes an array with the services supported by the LTI module
3917
 *
3918
 * @return array List of services
3919
 */
3920
function lti_get_services() {
3921
 
3922
    $services = array();
3923
    $definedservices = core_component::get_plugin_list('ltiservice');
3924
    foreach ($definedservices as $name => $location) {
3925
        $classname = "\\ltiservice_{$name}\\local\\service\\{$name}";
3926
        $services[] = new $classname();
3927
    }
3928
 
3929
    return $services;
3930
 
3931
}
3932
 
3933
/**
3934
 * Initializes an instance of the named service
3935
 *
3936
 * @param string $servicename Name of service
3937
 *
3938
 * @return bool|\mod_lti\local\ltiservice\service_base Service
3939
 */
3940
function lti_get_service_by_name($servicename) {
3941
 
3942
    $service = false;
3943
    $classname = "\\ltiservice_{$servicename}\\local\\service\\{$servicename}";
3944
    if (class_exists($classname)) {
3945
        $service = new $classname();
3946
    }
3947
 
3948
    return $service;
3949
 
3950
}
3951
 
3952
/**
3953
 * Finds a service by id
3954
 *
3955
 * @param \mod_lti\local\ltiservice\service_base[] $services Array of services
3956
 * @param string $resourceid  ID of resource
3957
 *
3958
 * @return mod_lti\local\ltiservice\service_base Service
3959
 */
3960
function lti_get_service_by_resource_id($services, $resourceid) {
3961
 
3962
    $service = false;
3963
    foreach ($services as $aservice) {
3964
        foreach ($aservice->get_resources() as $resource) {
3965
            if ($resource->get_id() === $resourceid) {
3966
                $service = $aservice;
3967
                break 2;
3968
            }
3969
        }
3970
    }
3971
 
3972
    return $service;
3973
 
3974
}
3975
 
3976
/**
3977
 * Initializes an array with the scopes for services supported by the LTI module
3978
 * and authorized for this particular tool instance.
3979
 *
3980
 * @param object $type  LTI tool type
3981
 * @param array  $typeconfig  LTI tool type configuration
3982
 *
3983
 * @return array List of scopes
3984
 */
3985
function lti_get_permitted_service_scopes($type, $typeconfig) {
3986
 
3987
    $services = lti_get_services();
3988
    $scopes = array();
3989
    foreach ($services as $service) {
3990
        $service->set_type($type);
3991
        $service->set_typeconfig($typeconfig);
3992
        $servicescopes = $service->get_permitted_scopes();
3993
        if (!empty($servicescopes)) {
3994
            $scopes = array_merge($scopes, $servicescopes);
3995
        }
3996
    }
3997
 
3998
    return $scopes;
3999
}
4000
 
4001
/**
4002
 * Extracts the named contexts from a tool proxy
4003
 *
4004
 * @param object $json
4005
 *
4006
 * @return array Contexts
4007
 */
4008
function lti_get_contexts($json) {
4009
 
4010
    $contexts = array();
4011
    if (isset($json->{'@context'})) {
4012
        foreach ($json->{'@context'} as $context) {
4013
            if (is_object($context)) {
4014
                $contexts = array_merge(get_object_vars($context), $contexts);
4015
            }
4016
        }
4017
    }
4018
 
4019
    return $contexts;
4020
 
4021
}
4022
 
4023
/**
4024
 * Converts an ID to a fully-qualified ID
4025
 *
4026
 * @param array $contexts
4027
 * @param string $id
4028
 *
4029
 * @return string Fully-qualified ID
4030
 */
4031
function lti_get_fqid($contexts, $id) {
4032
 
4033
    $parts = explode(':', $id, 2);
4034
    if (count($parts) > 1) {
4035
        if (array_key_exists($parts[0], $contexts)) {
4036
            $id = $contexts[$parts[0]] . $parts[1];
4037
        }
4038
    }
4039
 
4040
    return $id;
4041
 
4042
}
4043
 
4044
/**
4045
 * Returns the icon for the given tool type
4046
 *
4047
 * @param stdClass $type The tool type
4048
 *
4049
 * @return string The url to the tool type's corresponding icon
4050
 */
4051
function get_tool_type_icon_url(stdClass $type) {
4052
    global $OUTPUT;
4053
 
4054
    $iconurl = $type->secureicon;
4055
 
4056
    if (empty($iconurl)) {
4057
        $iconurl = $type->icon;
4058
    }
4059
 
4060
    if (empty($iconurl)) {
4061
        $iconurl = $OUTPUT->image_url('monologo', 'lti')->out();
4062
    }
4063
 
4064
    return $iconurl;
4065
}
4066
 
4067
/**
4068
 * Returns the edit url for the given tool type
4069
 *
4070
 * @param stdClass $type The tool type
4071
 *
4072
 * @return string The url to edit the tool type
4073
 */
4074
function get_tool_type_edit_url(stdClass $type) {
4075
    $url = new moodle_url('/mod/lti/typessettings.php',
4076
                          array('action' => 'update', 'id' => $type->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure'));
4077
    return $url->out();
4078
}
4079
 
4080
/**
4081
 * Returns the edit url for the given tool proxy.
4082
 *
4083
 * @param stdClass $proxy The tool proxy
4084
 *
4085
 * @return string The url to edit the tool type
4086
 */
4087
function get_tool_proxy_edit_url(stdClass $proxy) {
4088
    $url = new moodle_url('/mod/lti/registersettings.php',
4089
                          array('action' => 'update', 'id' => $proxy->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure'));
4090
    return $url->out();
4091
}
4092
 
4093
/**
4094
 * Returns the course url for the given tool type
4095
 *
4096
 * @param stdClass $type The tool type
4097
 *
4098
 * @return string The url to the course of the tool type, void if it is a site wide type
4099
 */
4100
function get_tool_type_course_url(stdClass $type) {
4101
    if ($type->course != 1) {
4102
        $url = new moodle_url('/course/view.php', array('id' => $type->course));
4103
        return $url->out();
4104
    }
4105
    return null;
4106
}
4107
 
4108
/**
4109
 * Returns the icon and edit urls for the tool type and the course url if it is a course type.
4110
 *
4111
 * @param stdClass $type The tool type
4112
 *
4113
 * @return array The urls of the tool type
4114
 */
4115
function get_tool_type_urls(stdClass $type) {
4116
    $courseurl = get_tool_type_course_url($type);
4117
 
4118
    $urls = array(
4119
        'icon' => get_tool_type_icon_url($type),
4120
        'edit' => get_tool_type_edit_url($type),
4121
    );
4122
 
4123
    if ($courseurl) {
4124
        $urls['course'] = $courseurl;
4125
    }
4126
 
4127
    $url = new moodle_url('/mod/lti/certs.php');
4128
    $urls['publickeyset'] = $url->out();
4129
    $url = new moodle_url('/mod/lti/token.php');
4130
    $urls['accesstoken'] = $url->out();
4131
    $url = new moodle_url('/mod/lti/auth.php');
4132
    $urls['authrequest'] = $url->out();
4133
 
4134
    return $urls;
4135
}
4136
 
4137
/**
4138
 * Returns the icon and edit urls for the tool proxy.
4139
 *
4140
 * @param stdClass $proxy The tool proxy
4141
 *
4142
 * @return array The urls of the tool proxy
4143
 */
4144
function get_tool_proxy_urls(stdClass $proxy) {
4145
    global $OUTPUT;
4146
 
4147
    $urls = array(
4148
        'icon' => $OUTPUT->image_url('monologo', 'lti')->out(),
4149
        'edit' => get_tool_proxy_edit_url($proxy),
4150
    );
4151
 
4152
    return $urls;
4153
}
4154
 
4155
/**
4156
 * Returns information on the current state of the tool type
4157
 *
4158
 * @param stdClass $type The tool type
4159
 *
4160
 * @return array An array with a text description of the state, and boolean for whether it is in each state:
4161
 * pending, configured, rejected, unknown
4162
 */
4163
function get_tool_type_state_info(stdClass $type) {
4164
    $isconfigured = false;
4165
    $ispending = false;
4166
    $isrejected = false;
4167
    $isunknown = false;
4168
    switch ($type->state) {
4169
        case LTI_TOOL_STATE_CONFIGURED:
4170
            $state = get_string('active', 'mod_lti');
4171
            $isconfigured = true;
4172
            break;
4173
        case LTI_TOOL_STATE_PENDING:
4174
            $state = get_string('pending', 'mod_lti');
4175
            $ispending = true;
4176
            break;
4177
        case LTI_TOOL_STATE_REJECTED:
4178
            $state = get_string('rejected', 'mod_lti');
4179
            $isrejected = true;
4180
            break;
4181
        default:
4182
            $state = get_string('unknownstate', 'mod_lti');
4183
            $isunknown = true;
4184
            break;
4185
    }
4186
 
4187
    return array(
4188
        'text' => $state,
4189
        'pending' => $ispending,
4190
        'configured' => $isconfigured,
4191
        'rejected' => $isrejected,
4192
        'unknown' => $isunknown
4193
    );
4194
}
4195
 
4196
/**
4197
 * Returns information on the configuration of the tool type
4198
 *
4199
 * @param stdClass $type The tool type
4200
 *
4201
 * @return array An array with configuration details
4202
 */
4203
function get_tool_type_config($type) {
4204
    global $CFG;
4205
    $platformid = $CFG->wwwroot;
4206
    $clientid = $type->clientid;
4207
    $deploymentid = $type->id;
4208
    $publickeyseturl = new moodle_url('/mod/lti/certs.php');
4209
    $publickeyseturl = $publickeyseturl->out();
4210
 
4211
    $accesstokenurl = new moodle_url('/mod/lti/token.php');
4212
    $accesstokenurl = $accesstokenurl->out();
4213
 
4214
    $authrequesturl = new moodle_url('/mod/lti/auth.php');
4215
    $authrequesturl = $authrequesturl->out();
4216
 
4217
    return array(
4218
        'platformid' => $platformid,
4219
        'clientid' => $clientid,
4220
        'deploymentid' => $deploymentid,
4221
        'publickeyseturl' => $publickeyseturl,
4222
        'accesstokenurl' => $accesstokenurl,
4223
        'authrequesturl' => $authrequesturl
4224
    );
4225
}
4226
 
4227
/**
4228
 * Returns a summary of each LTI capability this tool type requires in plain language
4229
 *
4230
 * @param stdClass $type The tool type
4231
 *
4232
 * @return array An array of text descriptions of each of the capabilities this tool type requires
4233
 */
4234
function get_tool_type_capability_groups($type) {
4235
    $capabilities = lti_get_enabled_capabilities($type);
4236
    $groups = array();
4237
    $hascourse = false;
4238
    $hasactivities = false;
4239
    $hasuseraccount = false;
4240
    $hasuserpersonal = false;
4241
 
4242
    foreach ($capabilities as $capability) {
4243
        // Bail out early if we've already found all groups.
4244
        if (count($groups) >= 4) {
4245
            continue;
4246
        }
4247
 
4248
        if (!$hascourse && preg_match('/^CourseSection/', $capability)) {
4249
            $hascourse = true;
4250
            $groups[] = get_string('courseinformation', 'mod_lti');
4251
        } else if (!$hasactivities && preg_match('/^ResourceLink/', $capability)) {
4252
            $hasactivities = true;
4253
            $groups[] = get_string('courseactivitiesorresources', 'mod_lti');
4254
        } else if (!$hasuseraccount && preg_match('/^User/', $capability) || preg_match('/^Membership/', $capability)) {
4255
            $hasuseraccount = true;
4256
            $groups[] = get_string('useraccountinformation', 'mod_lti');
4257
        } else if (!$hasuserpersonal && preg_match('/^Person/', $capability)) {
4258
            $hasuserpersonal = true;
4259
            $groups[] = get_string('userpersonalinformation', 'mod_lti');
4260
        }
4261
    }
4262
 
4263
    return $groups;
4264
}
4265
 
4266
 
4267
/**
4268
 * Returns the ids of each instance of this tool type
4269
 *
4270
 * @param stdClass $type The tool type
4271
 *
4272
 * @return array An array of ids of the instances of this tool type
4273
 */
4274
function get_tool_type_instance_ids($type) {
4275
    global $DB;
4276
 
4277
    return array_keys($DB->get_fieldset_select('lti', 'id', 'typeid = ?', array($type->id)));
4278
}
4279
 
4280
/**
4281
 * Serialises this tool type
4282
 *
4283
 * @param stdClass $type The tool type
4284
 *
4285
 * @return array An array of values representing this type
4286
 */
4287
function serialise_tool_type(stdClass $type) {
4288
    global $CFG;
4289
 
4290
    $capabilitygroups = get_tool_type_capability_groups($type);
4291
    $instanceids = get_tool_type_instance_ids($type);
4292
    // Clean the name. We don't want tags here.
4293
    $name = clean_param($type->name, PARAM_NOTAGS);
4294
    if (!empty($type->description)) {
4295
        // Clean the description. We don't want tags here.
4296
        $description = clean_param($type->description, PARAM_NOTAGS);
4297
    } else {
4298
        $description = get_string('editdescription', 'mod_lti');
4299
    }
4300
    return array(
4301
        'id' => $type->id,
4302
        'name' => $name,
4303
        'description' => $description,
4304
        'urls' => get_tool_type_urls($type),
4305
        'state' => get_tool_type_state_info($type),
4306
        'platformid' => $CFG->wwwroot,
4307
        'clientid' => $type->clientid,
4308
        'deploymentid' => $type->id,
4309
        'hascapabilitygroups' => !empty($capabilitygroups),
4310
        'capabilitygroups' => $capabilitygroups,
4311
        // Course ID of 1 means it's not linked to a course.
4312
        'courseid' => $type->course == 1 ? 0 : $type->course,
4313
        'instanceids' => $instanceids,
4314
        'instancecount' => count($instanceids)
4315
    );
4316
}
4317
 
4318
/**
4319
 * Loads the cartridge information into the tool type, if the launch url is for a cartridge file
4320
 *
4321
 * @param stdClass $type The tool type object to be filled in
4322
 * @since Moodle 3.1
4323
 */
4324
function lti_load_type_if_cartridge($type) {
4325
    if (!empty($type->lti_toolurl) && lti_is_cartridge($type->lti_toolurl)) {
4326
        lti_load_type_from_cartridge($type->lti_toolurl, $type);
4327
    }
4328
}
4329
 
4330
/**
4331
 * Loads the cartridge information into the new tool, if the launch url is for a cartridge file
4332
 *
4333
 * @param stdClass $lti The tools config
4334
 * @since Moodle 3.1
4335
 */
4336
function lti_load_tool_if_cartridge($lti) {
4337
    if (!empty($lti->toolurl) && lti_is_cartridge($lti->toolurl)) {
4338
        lti_load_tool_from_cartridge($lti->toolurl, $lti);
4339
    }
4340
}
4341
 
4342
/**
4343
 * Determines if the given url is for a IMS basic cartridge
4344
 *
4345
 * @param  string $url The url to be checked
4346
 * @return True if the url is for a cartridge
4347
 * @since Moodle 3.1
4348
 */
4349
function lti_is_cartridge($url) {
4350
    // If it is empty, it's not a cartridge.
4351
    if (empty($url)) {
4352
        return false;
4353
    }
4354
    // If it has xml at the end of the url, it's a cartridge.
4355
    if (preg_match('/\.xml$/', $url)) {
4356
        return true;
4357
    }
4358
    // Even if it doesn't have .xml, load the url to check if it's a cartridge..
4359
    try {
4360
        $toolinfo = lti_load_cartridge($url,
4361
            array(
4362
                "launch_url" => "launchurl"
4363
            )
4364
        );
4365
        if (!empty($toolinfo['launchurl'])) {
4366
            return true;
4367
        }
4368
    } catch (moodle_exception $e) {
4369
        return false; // Error loading the xml, so it's not a cartridge.
4370
    }
4371
    return false;
4372
}
4373
 
4374
/**
4375
 * Allows you to load settings for an external tool type from an IMS cartridge.
4376
 *
4377
 * @param  string   $url     The URL to the cartridge
4378
 * @param  stdClass $type    The tool type object to be filled in
4379
 * @throws moodle_exception if the cartridge could not be loaded correctly
4380
 * @since Moodle 3.1
4381
 */
4382
function lti_load_type_from_cartridge($url, $type) {
4383
    $toolinfo = lti_load_cartridge($url,
4384
        array(
4385
            "title" => "lti_typename",
4386
            "launch_url" => "lti_toolurl",
4387
            "description" => "lti_description",
4388
            "icon" => "lti_icon",
4389
            "secure_icon" => "lti_secureicon"
4390
        ),
4391
        array(
4392
            "icon_url" => "lti_extension_icon",
4393
            "secure_icon_url" => "lti_extension_secureicon"
4394
        )
4395
    );
4396
    // If an activity name exists, unset the cartridge name so we don't override it.
4397
    if (isset($type->lti_typename)) {
4398
        unset($toolinfo['lti_typename']);
4399
    }
4400
 
4401
    // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
4402
    if (empty($toolinfo['lti_icon']) && !empty($toolinfo['lti_extension_icon'])) {
4403
        $toolinfo['lti_icon'] = $toolinfo['lti_extension_icon'];
4404
    }
4405
    unset($toolinfo['lti_extension_icon']);
4406
 
4407
    if (empty($toolinfo['lti_secureicon']) && !empty($toolinfo['lti_extension_secureicon'])) {
4408
        $toolinfo['lti_secureicon'] = $toolinfo['lti_extension_secureicon'];
4409
    }
4410
    unset($toolinfo['lti_extension_secureicon']);
4411
 
4412
    // Ensure Custom icons aren't overridden by cartridge params.
4413
    if (!empty($type->lti_icon)) {
4414
        unset($toolinfo['lti_icon']);
4415
    }
4416
 
4417
    if (!empty($type->lti_secureicon)) {
4418
        unset($toolinfo['lti_secureicon']);
4419
    }
4420
 
4421
    foreach ($toolinfo as $property => $value) {
4422
        $type->$property = $value;
4423
    }
4424
}
4425
 
4426
/**
4427
 * Allows you to load in the configuration for an external tool from an IMS cartridge.
4428
 *
4429
 * @param  string   $url    The URL to the cartridge
4430
 * @param  stdClass $lti    LTI object
4431
 * @throws moodle_exception if the cartridge could not be loaded correctly
4432
 * @since Moodle 3.1
4433
 */
4434
function lti_load_tool_from_cartridge($url, $lti) {
4435
    $toolinfo = lti_load_cartridge($url,
4436
        array(
4437
            "title" => "name",
4438
            "launch_url" => "toolurl",
4439
            "secure_launch_url" => "securetoolurl",
4440
            "description" => "intro",
4441
            "icon" => "icon",
4442
            "secure_icon" => "secureicon"
4443
        ),
4444
        array(
4445
            "icon_url" => "extension_icon",
4446
            "secure_icon_url" => "extension_secureicon"
4447
        )
4448
    );
4449
    // If an activity name exists, unset the cartridge name so we don't override it.
4450
    if (isset($lti->name)) {
4451
        unset($toolinfo['name']);
4452
    }
4453
 
4454
    // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
4455
    if (empty($toolinfo['icon']) && !empty($toolinfo['extension_icon'])) {
4456
        $toolinfo['icon'] = $toolinfo['extension_icon'];
4457
    }
4458
    unset($toolinfo['extension_icon']);
4459
 
4460
    if (empty($toolinfo['secureicon']) && !empty($toolinfo['extension_secureicon'])) {
4461
        $toolinfo['secureicon'] = $toolinfo['extension_secureicon'];
4462
    }
4463
    unset($toolinfo['extension_secureicon']);
4464
 
4465
    foreach ($toolinfo as $property => $value) {
4466
        $lti->$property = $value;
4467
    }
4468
}
4469
 
4470
/**
4471
 * Search for a tag within an XML DOMDocument
4472
 *
4473
 * @param  string $url The url of the cartridge to be loaded
4474
 * @param  array  $map The map of tags to keys in the return array
4475
 * @param  array  $propertiesmap The map of properties to keys in the return array
4476
 * @return array An associative array with the given keys and their values from the cartridge
4477
 * @throws moodle_exception if the cartridge could not be loaded correctly
4478
 * @since Moodle 3.1
4479
 */
4480
function lti_load_cartridge($url, $map, $propertiesmap = array()) {
4481
    global $CFG;
4482
    require_once($CFG->libdir. "/filelib.php");
4483
 
4484
    $curl = new curl();
4485
    $response = $curl->get($url);
4486
 
4487
    // Got a completely empty response (real or error), cannot process this with
4488
    // DOMDocument::loadXML() because it errors with ValueError. So let's throw
4489
    // the moodle_exception before waiting to examine the errors later.
4490
    if (trim($response) === '') {
4491
        throw new moodle_exception('errorreadingfile', '', '', $url);
4492
    }
4493
 
4494
    // TODO MDL-46023 Replace this code with a call to the new library.
4495
    $origerrors = libxml_use_internal_errors(true);
4496
    libxml_clear_errors();
4497
 
4498
    $document = new DOMDocument();
4499
    @$document->loadXML($response, LIBXML_NONET);
4500
 
4501
    $cartridge = new DomXpath($document);
4502
 
4503
    $errors = libxml_get_errors();
4504
 
4505
    libxml_clear_errors();
4506
    libxml_use_internal_errors($origerrors);
4507
 
4508
    if (count($errors) > 0) {
4509
        $message = 'Failed to load cartridge.';
4510
        foreach ($errors as $error) {
4511
            $message .= "\n" . trim($error->message, "\n\r\t .") . " at line " . $error->line;
4512
        }
4513
        throw new moodle_exception('errorreadingfile', '', '', $url, $message);
4514
    }
4515
 
4516
    $toolinfo = array();
4517
    foreach ($map as $tag => $key) {
4518
        $value = get_tag($tag, $cartridge);
4519
        if ($value) {
4520
            $toolinfo[$key] = $value;
4521
        }
4522
    }
4523
    if (!empty($propertiesmap)) {
4524
        foreach ($propertiesmap as $property => $key) {
4525
            $value = get_tag("property", $cartridge, $property);
4526
            if ($value) {
4527
                $toolinfo[$key] = $value;
4528
            }
4529
        }
4530
    }
4531
 
4532
    return $toolinfo;
4533
}
4534
 
4535
/**
4536
 * Search for a tag within an XML DOMDocument
4537
 *
4538
 * @param  stdClass $tagname The name of the tag to search for
4539
 * @param  XPath    $xpath   The XML to find the tag in
4540
 * @param  XPath    $attribute The attribute to search for (if we should search for a child node with the given
4541
 * value for the name attribute
4542
 * @since Moodle 3.1
4543
 */
4544
function get_tag($tagname, $xpath, $attribute = null) {
4545
    if ($attribute) {
4546
        $result = $xpath->query('//*[local-name() = \'' . $tagname . '\'][@name="' . $attribute . '"]');
4547
    } else {
4548
        $result = $xpath->query('//*[local-name() = \'' . $tagname . '\']');
4549
    }
4550
    if ($result->length > 0) {
4551
        return $result->item(0)->nodeValue;
4552
    }
4553
    return null;
4554
}
4555
 
4556
/**
4557
 * Create a new access token.
4558
 *
4559
 * @param int $typeid Tool type ID
4560
 * @param string[] $scopes Scopes permitted for new token
4561
 *
4562
 * @return stdClass Access token
4563
 */
4564
function lti_new_access_token($typeid, $scopes) {
4565
    global $DB;
4566
 
4567
    // Make sure the token doesn't exist (even if it should be almost impossible with the random generation).
4568
    $numtries = 0;
4569
    do {
4570
        $numtries ++;
4571
        $generatedtoken = md5(uniqid(rand(), 1));
4572
        if ($numtries > 5) {
4573
            throw new moodle_exception('Failed to generate LTI access token');
4574
        }
4575
    } while ($DB->record_exists('lti_access_tokens', array('token' => $generatedtoken)));
4576
    $newtoken = new stdClass();
4577
    $newtoken->typeid = $typeid;
4578
    $newtoken->scope = json_encode(array_values($scopes));
4579
    $newtoken->token = $generatedtoken;
4580
 
4581
    $newtoken->timecreated = time();
4582
    $newtoken->validuntil = $newtoken->timecreated + LTI_ACCESS_TOKEN_LIFE;
4583
    $newtoken->lastaccess = null;
4584
 
4585
    $DB->insert_record('lti_access_tokens', $newtoken);
4586
 
4587
    return $newtoken;
4588
 
4589
}
4590
 
4591
 
4592
/**
4593
 * Wrapper for function libxml_disable_entity_loader() deprecated in PHP 8
4594
 *
4595
 * Method was deprecated in PHP 8 and it shows deprecation message. However it is still
4596
 * required in the previous versions on PHP. While Moodle supports both PHP 7 and 8 we need to keep it.
4597
 * @see https://php.watch/versions/8.0/libxml_disable_entity_loader-deprecation
4598
 *
4599
 * @param bool $value
4600
 * @return bool
4601
 *
4602
 * @deprecated since Moodle 4.3
4603
 */
4604
function lti_libxml_disable_entity_loader(bool $value): bool {
4605
    debugging(__FUNCTION__ . '() is deprecated, please do not use it any more', DEBUG_DEVELOPER);
4606
    return true;
4607
}