Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

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