MDL-66920 mod_lti: Allow usage of both JWKS URI and Public Key
[moodle.git] / mod / lti / tests / locallib_test.php
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.
35 /**
36  * This file contains unit tests for (some of) lti/locallib.php
37  *
38  * @package    mod_lti
39  * @category   phpunit
40  * @copyright  2009 Marc Alier, Jordi Piguillem, Nikolas Galanis
41  * @copyright  2009 Universitat Politecnica de Catalunya http://www.upc.edu
42  * @author     Charles Severance csev@unmich.edu
43  * @author     Marc Alier (marc.alier@upc.edu)
44  * @author     Jordi Piguillem
45  * @author     Nikolas Galanis
46  * @author     Chris Scribner
47  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
48  */
50 defined('MOODLE_INTERNAL') || die;
52 global $CFG;
53 require_once($CFG->dirroot . '/mod/lti/locallib.php');
54 require_once($CFG->dirroot . '/mod/lti/servicelib.php');
56 /**
57  * Local library tests
58  *
59  * @package    mod_lti
60  * @copyright  Copyright (c) 2012 Moodlerooms Inc. (http://www.moodlerooms.com)
61  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
62  */
63 class mod_lti_locallib_testcase extends advanced_testcase {
65     public function test_split_custom_parameters() {
66         $this->resetAfterTest();
68         $tool = new stdClass();
69         $tool->enabledcapability = '';
70         $tool->parameter = '';
71         $tool->ltiversion = 'LTI-1p0';
72         $this->assertEquals(lti_split_custom_parameters(null, $tool, array(), "x=1\ny=2", false),
73             array('custom_x' => '1', 'custom_y' => '2'));
75         // Check params with caps.
76         $this->assertEquals(lti_split_custom_parameters(null, $tool, array(), "X=1", true),
77             array('custom_x' => '1', 'custom_X' => '1'));
79         // Removed repeat of previous test with a semicolon separator.
81         $this->assertEquals(lti_split_custom_parameters(null, $tool, array(), 'Review:Chapter=1.2.56', true),
82             array(
83                 'custom_review_chapter' => '1.2.56',
84                 'custom_Review:Chapter' => '1.2.56'));
86         $this->assertEquals(lti_split_custom_parameters(null, $tool, array(),
87             'Complex!@#$^*(){}[]KEY=Complex!@#$^*;(){}[]½Value', true),
88             array(
89                 'custom_complex____________key' => 'Complex!@#$^*;(){}[]½Value',
90                 'custom_Complex!@#$^*(){}[]KEY' => 'Complex!@#$^*;(){}[]½Value'));
92         // Test custom parameter that returns $USER property.
93         $user = $this->getDataGenerator()->create_user(array('middlename' => 'SOMETHING'));
94         $this->setUser($user);
95         $this->assertEquals(array('custom_x' => '1', 'custom_y' => 'SOMETHING'),
96             lti_split_custom_parameters(null, $tool, array(), "x=1\ny=\$Person.name.middle", false));
97     }
99     /**
100      * This test has been disabled because the test-tool is
101      * being moved and probably it won't work anymore for this.
102      * We should be testing here local stuff only and leave
103      * outside-checks to the conformance tests. MDL-30347
104      */
105     public function disabled_test_sign_parameters() {
106         $correct = array ( 'context_id' => '12345', 'context_label' => 'SI124', 'context_title' => 'Social Computing',
107             'ext_submit' => 'Click Me', 'lti_message_type' => 'basic-lti-launch-request', 'lti_version' => 'LTI-1p0',
108             'oauth_consumer_key' => 'lmsng.school.edu', 'oauth_nonce' => '47458148e33a8f9dafb888c3684cf476',
109             'oauth_signature' => 'qWgaBIezihCbeHgcwUy14tZcyDQ=', 'oauth_signature_method' => 'HMAC-SHA1',
110             'oauth_timestamp' => '1307141660', 'oauth_version' => '1.0', 'resource_link_id' => '123',
111             'resource_link_title' => 'Weekly Blog', 'roles' => 'Learner', 'tool_consumer_instance_guid' => 'lmsng.school.edu',
112             'user_id' => '789');
114         $requestparams = array('resource_link_id' => '123', 'resource_link_title' => 'Weekly Blog', 'user_id' => '789',
115             'roles' => 'Learner', 'context_id' => '12345', 'context_label' => 'SI124', 'context_title' => 'Social Computing');
117         $parms = lti_sign_parameters($requestparams, 'http://www.imsglobal.org/developer/LTI/tool.php', 'POST',
118             'lmsng.school.edu', 'secret', 'Click Me', 'lmsng.school.edu' /*, $org_desc*/);
119         $this->assertTrue(isset($parms['oauth_nonce']));
120         $this->assertTrue(isset($parms['oauth_signature']));
121         $this->assertTrue(isset($parms['oauth_timestamp']));
123         // Those things that are hard to mock.
124         $correct['oauth_nonce'] = $parms['oauth_nonce'];
125         $correct['oauth_signature'] = $parms['oauth_signature'];
126         $correct['oauth_timestamp'] = $parms['oauth_timestamp'];
127         ksort($parms);
128         ksort($correct);
129         $this->assertEquals($parms, $correct);
130     }
132     /**
133      * This test has been disabled because, since its creation,
134      * the sourceId generation has changed and surely this is outdated.
135      * Some day these should be replaced by proper tests, but until then
136      * conformance tests say this is working. MDL-30347
137      */
138     public function disabled_test_parse_grade_replace_message() {
139         $message = '
140             <imsx_POXEnvelopeRequest xmlns = "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
141               <imsx_POXHeader>
142                 <imsx_POXRequestHeaderInfo>
143                   <imsx_version>V1.0</imsx_version>
144                   <imsx_messageIdentifier>999998123</imsx_messageIdentifier>
145                 </imsx_POXRequestHeaderInfo>
146               </imsx_POXHeader>
147               <imsx_POXBody>
148                 <replaceResultRequest>
149                   <resultRecord>
150                     <sourcedGUID>
151                       <sourcedId>' .
152             '{&quot;data&quot;:{&quot;instanceid&quot;:&quot;2&quot;,&quot;userid&quot;:&quot;2&quot;},&quot;hash&quot;:' .
153             '&quot;0b5078feab59b9938c333ceaae21d8e003a7b295e43cdf55338445254421076b&quot;}' .
154                       '</sourcedId>
155                     </sourcedGUID>
156                     <result>
157                       <resultScore>
158                         <language>en-us</language>
159                         <textString>0.92</textString>
160                       </resultScore>
161                     </result>
162                   </resultRecord>
163                 </replaceResultRequest>
164               </imsx_POXBody>
165             </imsx_POXEnvelopeRequest>
166 ';
168         $parsed = lti_parse_grade_replace_message(new SimpleXMLElement($message));
170         $this->assertEquals($parsed->userid, '2');
171         $this->assertEquals($parsed->instanceid, '2');
172         $this->assertEquals($parsed->sourcedidhash, '0b5078feab59b9938c333ceaae21d8e003a7b295e43cdf55338445254421076b');
174         $ltiinstance = (object)array('servicesalt' => '4e5fcc06de1d58.44963230');
176         lti_verify_sourcedid($ltiinstance, $parsed);
177     }
179     public function test_lti_ensure_url_is_https() {
180         $this->assertEquals('https://moodle.org', lti_ensure_url_is_https('http://moodle.org'));
181         $this->assertEquals('https://moodle.org', lti_ensure_url_is_https('moodle.org'));
182         $this->assertEquals('https://moodle.org', lti_ensure_url_is_https('https://moodle.org'));
183     }
185     /**
186      * Test lti_get_url_thumbprint against various URLs
187      */
188     public function test_lti_get_url_thumbprint() {
189         // Note: trailing and double slash are expected right now.  Must evaluate if it must be removed at some point.
190         $this->assertEquals('moodle.org/', lti_get_url_thumbprint('http://MOODLE.ORG'));
191         $this->assertEquals('moodle.org/', lti_get_url_thumbprint('http://www.moodle.org'));
192         $this->assertEquals('moodle.org/', lti_get_url_thumbprint('https://www.moodle.org'));
193         $this->assertEquals('moodle.org/', lti_get_url_thumbprint('moodle.org'));
194         $this->assertEquals('moodle.org//this/is/moodle', lti_get_url_thumbprint('http://moodle.org/this/is/moodle'));
195         $this->assertEquals('moodle.org//this/is/moodle', lti_get_url_thumbprint('https://moodle.org/this/is/moodle'));
196         $this->assertEquals('moodle.org//this/is/moodle', lti_get_url_thumbprint('moodle.org/this/is/moodle'));
197         $this->assertEquals('moodle.org//this/is/moodle', lti_get_url_thumbprint('moodle.org/this/is/moodle?'));
198         $this->assertEquals('moodle.org//this/is/moodle?foo=bar', lti_get_url_thumbprint('moodle.org/this/is/moodle?foo=bar'));
199     }
201     /*
202      * Verify that lti_build_request does handle resource_link_id as expected
203      */
204     public function test_lti_buid_request_resource_link_id() {
205         $this->resetAfterTest();
207         self::setUser($this->getDataGenerator()->create_user());
208         $course   = $this->getDataGenerator()->create_course();
209         $instance = $this->getDataGenerator()->create_module('lti', array(
210             'intro'       => "<p>This</p>\nhas\r\n<p>some</p>\nnew\n\rlines",
211             'introformat' => FORMAT_HTML,
212             'course'      => $course->id,
213         ));
215         $typeconfig = array(
216             'acceptgrades'     => 1,
217             'forcessl'         => 0,
218             'sendname'         => 2,
219             'sendemailaddr'    => 2,
220             'customparameters' => '',
221         );
223         // Normal call, we expect $instance->id to be used as resource_link_id.
224         $params = lti_build_request($instance, $typeconfig, $course, null);
225         $this->assertSame($instance->id, $params['resource_link_id']);
227         // If there is a resource_link_id set, it gets precedence.
228         $instance->resource_link_id = $instance->id + 99;
229         $params = lti_build_request($instance, $typeconfig, $course, null);
230         $this->assertSame($instance->resource_link_id, $params['resource_link_id']);
232         // With none set, resource_link_id is not set either.
233         unset($instance->id);
234         unset($instance->resource_link_id);
235         $params = lti_build_request($instance, $typeconfig, $course, null);
236         $this->assertArrayNotHasKey('resource_link_id', $params);
237     }
239     /**
240      * Test lti_build_request's resource_link_description and ensure
241      * that the newlines in the description are correct.
242      */
243     public function test_lti_build_request_description() {
244         $this->resetAfterTest();
246         self::setUser($this->getDataGenerator()->create_user());
247         $course   = $this->getDataGenerator()->create_course();
248         $instance = $this->getDataGenerator()->create_module('lti', array(
249             'intro'       => "<p>This</p>\nhas\r\n<p>some</p>\nnew\n\rlines",
250             'introformat' => FORMAT_HTML,
251             'course'      => $course->id,
252         ));
254         $typeconfig = array(
255             'acceptgrades'     => 1,
256             'forcessl'         => 0,
257             'sendname'         => 2,
258             'sendemailaddr'    => 2,
259             'customparameters' => '',
260         );
262         $params = lti_build_request($instance, $typeconfig, $course, null);
264         $ncount = substr_count($params['resource_link_description'], "\n");
265         $this->assertGreaterThan(0, $ncount);
267         $rcount = substr_count($params['resource_link_description'], "\r");
268         $this->assertGreaterThan(0, $rcount);
270         $this->assertEquals($ncount, $rcount, 'The number of \n characters should be the same as the number of \r characters');
272         $rncount = substr_count($params['resource_link_description'], "\r\n");
273         $this->assertGreaterThan(0, $rncount);
275         $this->assertEquals($ncount, $rncount, 'All newline characters should be a combination of \r\n');
276     }
278     /**
279      * Tests lti_prepare_type_for_save's handling of the "Force SSL" configuration.
280      */
281     public function test_lti_prepare_type_for_save_forcessl() {
282         $type = new stdClass();
283         $config = new stdClass();
285         // Try when the forcessl config property is not set.
286         lti_prepare_type_for_save($type, $config);
287         $this->assertObjectHasAttribute('lti_forcessl', $config);
288         $this->assertEquals(0, $config->lti_forcessl);
289         $this->assertEquals(0, $type->forcessl);
291         // Try when forcessl config property is set.
292         $config->lti_forcessl = 1;
293         lti_prepare_type_for_save($type, $config);
294         $this->assertObjectHasAttribute('lti_forcessl', $config);
295         $this->assertEquals(1, $config->lti_forcessl);
296         $this->assertEquals(1, $type->forcessl);
298         // Try when forcessl config property is set to 0.
299         $config->lti_forcessl = 0;
300         lti_prepare_type_for_save($type, $config);
301         $this->assertObjectHasAttribute('lti_forcessl', $config);
302         $this->assertEquals(0, $config->lti_forcessl);
303         $this->assertEquals(0, $type->forcessl);
304     }
306     /**
307      * Tests lti_load_type_from_cartridge and lti_load_type_if_cartridge
308      */
309     public function test_lti_load_type_from_cartridge() {
310         $type = new stdClass();
311         $type->lti_toolurl = $this->getExternalTestFileUrl('/ims_cartridge_basic_lti_link.xml');
313         lti_load_type_if_cartridge($type);
315         $this->assertEquals('Example tool', $type->lti_typename);
316         $this->assertEquals('Example tool description', $type->lti_description);
317         $this->assertEquals('http://www.example.com/lti/provider.php', $type->lti_toolurl);
318         $this->assertEquals('http://download.moodle.org/unittest/test.jpg', $type->lti_icon);
319         $this->assertEquals('https://download.moodle.org/unittest/test.jpg', $type->lti_secureicon);
320     }
322     /**
323      * Tests lti_load_tool_from_cartridge and lti_load_tool_if_cartridge
324      */
325     public function test_lti_load_tool_from_cartridge() {
326         $lti = new stdClass();
327         $lti->toolurl = $this->getExternalTestFileUrl('/ims_cartridge_basic_lti_link.xml');
329         lti_load_tool_if_cartridge($lti);
331         $this->assertEquals('Example tool', $lti->name);
332         $this->assertEquals('Example tool description', $lti->intro);
333         $this->assertEquals('http://www.example.com/lti/provider.php', $lti->toolurl);
334         $this->assertEquals('https://www.example.com/lti/provider.php', $lti->securetoolurl);
335         $this->assertEquals('http://download.moodle.org/unittest/test.jpg', $lti->icon);
336         $this->assertEquals('https://download.moodle.org/unittest/test.jpg', $lti->secureicon);
337     }
339     /**
340      * Tests for lti_build_content_item_selection_request().
341      */
342     public function test_lti_build_content_item_selection_request() {
343         $this->resetAfterTest();
345         $this->setAdminUser();
346         // Create a tool proxy.
347         $proxy = mod_lti_external::create_tool_proxy('Test proxy', $this->getExternalTestFileUrl('/test.html'), array(), array());
349         // Create a tool type, associated with that proxy.
350         $type = new stdClass();
351         $data = new stdClass();
352         $data->lti_contentitem = true;
353         $type->state = LTI_TOOL_STATE_CONFIGURED;
354         $type->name = "Test tool";
355         $type->description = "Example description";
356         $type->toolproxyid = $proxy->id;
357         $type->baseurl = $this->getExternalTestFileUrl('/test.html');
359         $typeid = lti_add_type($type, $data);
361         $typeconfig = lti_get_type_config($typeid);
363         $course = $this->getDataGenerator()->create_course();
364         $returnurl = new moodle_url('/');
366         // Default parameters.
367         $result = lti_build_content_item_selection_request($typeid, $course, $returnurl);
368         $this->assertNotEmpty($result);
369         $this->assertNotEmpty($result->params);
370         $this->assertNotEmpty($result->url);
371         $params = $result->params;
372         $url = $result->url;
373         $this->assertEquals($typeconfig['toolurl'], $url);
374         $this->assertEquals('ContentItemSelectionRequest', $params['lti_message_type']);
375         $this->assertEquals(LTI_VERSION_1, $params['lti_version']);
376         $this->assertEquals('application/vnd.ims.lti.v1.ltilink', $params['accept_media_types']);
377         $this->assertEquals('frame,iframe,window', $params['accept_presentation_document_targets']);
378         $this->assertEquals($returnurl->out(false), $params['content_item_return_url']);
379         $this->assertEquals('false', $params['accept_unsigned']);
380         $this->assertEquals('false', $params['accept_multiple']);
381         $this->assertEquals('false', $params['accept_copy_advice']);
382         $this->assertEquals('false', $params['auto_create']);
383         $this->assertEquals($type->name, $params['title']);
384         $this->assertFalse(isset($params['resource_link_id']));
385         $this->assertFalse(isset($params['resource_link_title']));
386         $this->assertFalse(isset($params['resource_link_description']));
387         $this->assertFalse(isset($params['launch_presentation_return_url']));
388         $this->assertFalse(isset($params['lis_result_sourcedid']));
389         $this->assertEquals($params['tool_consumer_instance_guid'], 'www.example.com');
391         // Custom parameters.
392         $title = 'My custom title';
393         $text = 'This is the tool description';
394         $mediatypes = ['image/*', 'video/*'];
395         $targets = ['embed', 'iframe'];
396         $result = lti_build_content_item_selection_request($typeid, $course, $returnurl, $title, $text, $mediatypes, $targets,
397             true, true, true, true, true);
398         $this->assertNotEmpty($result);
399         $this->assertNotEmpty($result->params);
400         $this->assertNotEmpty($result->url);
401         $params = $result->params;
402         $this->assertEquals(implode(',', $mediatypes), $params['accept_media_types']);
403         $this->assertEquals(implode(',', $targets), $params['accept_presentation_document_targets']);
404         $this->assertEquals('true', $params['accept_unsigned']);
405         $this->assertEquals('true', $params['accept_multiple']);
406         $this->assertEquals('true', $params['accept_copy_advice']);
407         $this->assertEquals('true', $params['auto_create']);
408         $this->assertEquals($title, $params['title']);
409         $this->assertEquals($text, $params['text']);
411         // Invalid flag values.
412         $result = lti_build_content_item_selection_request($typeid, $course, $returnurl, $title, $text, $mediatypes, $targets,
413             'aa', -1, 0, 1, 0xabc);
414         $this->assertNotEmpty($result);
415         $this->assertNotEmpty($result->params);
416         $this->assertNotEmpty($result->url);
417         $params = $result->params;
418         $this->assertEquals(implode(',', $mediatypes), $params['accept_media_types']);
419         $this->assertEquals(implode(',', $targets), $params['accept_presentation_document_targets']);
420         $this->assertEquals('false', $params['accept_unsigned']);
421         $this->assertEquals('false', $params['accept_multiple']);
422         $this->assertEquals('false', $params['accept_copy_advice']);
423         $this->assertEquals('false', $params['auto_create']);
424         $this->assertEquals($title, $params['title']);
425         $this->assertEquals($text, $params['text']);
426     }
428     /**
429      * Test for lti_build_content_item_selection_request() with nonexistent tool type ID parameter.
430      */
431     public function test_lti_build_content_item_selection_request_invalid_tooltype() {
432         $this->resetAfterTest();
434         $this->setAdminUser();
435         $course = $this->getDataGenerator()->create_course();
436         $returnurl = new moodle_url('/');
438         // Should throw Exception on non-existent tool type.
439         $this->expectException('moodle_exception');
440         lti_build_content_item_selection_request(1, $course, $returnurl);
441     }
443     /**
444      * Test for lti_build_content_item_selection_request() with invalid media types parameter.
445      */
446     public function test_lti_build_content_item_selection_request_invalid_mediatypes() {
447         $this->resetAfterTest();
449         $this->setAdminUser();
451         // Create a tool type, associated with that proxy.
452         $type = new stdClass();
453         $data = new stdClass();
454         $data->lti_contentitem = true;
455         $type->state = LTI_TOOL_STATE_CONFIGURED;
456         $type->name = "Test tool";
457         $type->description = "Example description";
458         $type->baseurl = $this->getExternalTestFileUrl('/test.html');
460         $typeid = lti_add_type($type, $data);
461         $course = $this->getDataGenerator()->create_course();
462         $returnurl = new moodle_url('/');
464         // Should throw coding_exception on non-array media types.
465         $mediatypes = 'image/*,video/*';
466         $this->expectException('coding_exception');
467         lti_build_content_item_selection_request($typeid, $course, $returnurl, '', '', $mediatypes);
468     }
470     /**
471      * Test for lti_build_content_item_selection_request() with invalid presentation targets parameter.
472      */
473     public function test_lti_build_content_item_selection_request_invalid_presentationtargets() {
474         $this->resetAfterTest();
476         $this->setAdminUser();
478         // Create a tool type, associated with that proxy.
479         $type = new stdClass();
480         $data = new stdClass();
481         $data->lti_contentitem = true;
482         $type->state = LTI_TOOL_STATE_CONFIGURED;
483         $type->name = "Test tool";
484         $type->description = "Example description";
485         $type->baseurl = $this->getExternalTestFileUrl('/test.html');
487         $typeid = lti_add_type($type, $data);
488         $course = $this->getDataGenerator()->create_course();
489         $returnurl = new moodle_url('/');
491         // Should throw coding_exception on non-array presentation targets.
492         $targets = 'frame,iframe';
493         $this->expectException('coding_exception');
494         lti_build_content_item_selection_request($typeid, $course, $returnurl, '', '', [], $targets);
495     }
497     /**
498      * Provider for test_lti_get_best_tool_by_url.
499      *
500      * @return array of [urlToTest, expectedTool, allTools]
501      */
502     public function lti_get_best_tool_by_url_provider() {
503         $tools = [
504             (object) [
505                 'name' => 'Here',
506                 'baseurl' => 'https://example.com/i/am/?where=here',
507                 'tooldomain' => 'example.com',
508                 'state' => LTI_TOOL_STATE_CONFIGURED,
509                 'course' => SITEID
510             ],
511             (object) [
512                 'name' => 'There',
513                 'baseurl' => 'https://example.com/i/am/?where=there',
514                 'tooldomain' => 'example.com',
515                 'state' => LTI_TOOL_STATE_CONFIGURED,
516                 'course' => SITEID
517             ],
518             (object) [
519                 'name' => 'Not here',
520                 'baseurl' => 'https://example.com/i/am/?where=not/here',
521                 'tooldomain' => 'example.com',
522                 'state' => LTI_TOOL_STATE_CONFIGURED,
523                 'course' => SITEID
524             ],
525             (object) [
526                 'name' => 'Here',
527                 'baseurl' => 'https://example.com/i/am/',
528                 'tooldomain' => 'example.com',
529                 'state' => LTI_TOOL_STATE_CONFIGURED,
530                 'course' => SITEID
531             ],
532             (object) [
533                 'name' => 'Here',
534                 'baseurl' => 'https://example.com/i/was',
535                 'tooldomain' => 'example.com',
536                 'state' => LTI_TOOL_STATE_CONFIGURED,
537                 'course' => SITEID
538             ],
539             (object) [
540                 'name' => 'Here',
541                 'baseurl' => 'https://badexample.com/i/am/?where=here',
542                 'tooldomain' => 'badexample.com',
543                 'state' => LTI_TOOL_STATE_CONFIGURED,
544                 'course' => SITEID
545             ],
546         ];
548         $data = [
549             [
550                 'url' => $tools[0]->baseurl,
551                 'expected' => $tools[0],
552             ],
553             [
554                 'url' => $tools[1]->baseurl,
555                 'expected' => $tools[1],
556             ],
557             [
558                 'url' => $tools[2]->baseurl,
559                 'expected' => $tools[2],
560             ],
561             [
562                 'url' => $tools[3]->baseurl,
563                 'expected' => $tools[3],
564             ],
565             [
566                 'url' => $tools[4]->baseurl,
567                 'expected' => $tools[4],
568             ],
569             [
570                 'url' => $tools[5]->baseurl,
571                 'expected' => $tools[5],
572             ],
573             [
574                 'url' => 'https://nomatch.com/i/am/',
575                 'expected' => null
576             ],
577             [
578                 'url' => 'https://example.com',
579                 'expected' => null
580             ],
581             [
582                 'url' => 'https://example.com/i/am/?where=unknown',
583                 'expected' => $tools[3]
584             ]
585         ];
587         // Construct the final array as required by the provider API. Each row
588         // of the array contains the URL to test, the expected tool, and
589         // the complete list of tools.
590         return array_map(function($data) use ($tools) {
591             return [$data['url'], $data['expected'], $tools];
592         }, $data);
593     }
595     /**
596      * Test lti_get_best_tool_by_url.
597      *
598      * @dataProvider lti_get_best_tool_by_url_provider
599      * @param string $url The URL to test.
600      * @param object $expected The expected tool matching the URL.
601      * @param array $tools The pool of tools to match the URL with.
602      */
603     public function test_lti_get_best_tool_by_url($url, $expected, $tools) {
604         $actual = lti_get_best_tool_by_url($url, $tools, null);
605         $this->assertSame($expected, $actual);
606     }
608     /**
609      * Test lti_get_jwt_message_type_mapping().
610      */
611     public function test_lti_get_jwt_message_type_mapping() {
612         $mapping = [
613             'basic-lti-launch-request' => 'LtiResourceLinkRequest',
614             'ContentItemSelectionRequest' => 'LtiDeepLinkingRequest',
615             'LtiDeepLinkingResponse' => 'ContentItemSelection',
616         ];
618         $this->assertEquals($mapping, lti_get_jwt_message_type_mapping());
619     }
621     /**
622      * Test lti_get_jwt_claim_mapping()
623      */
624     public function test_lti_get_jwt_claim_mapping() {
625         $mapping = [
626             'accept_copy_advice' => [
627                 'suffix' => 'dl',
628                 'group' => 'deep_linking_settings',
629                 'claim' => 'accept_copy_advice',
630                 'isarray' => false
631             ],
632             'accept_media_types' => [
633                 'suffix' => 'dl',
634                 'group' => 'deep_linking_settings',
635                 'claim' => 'accept_media_types',
636                 'isarray' => true
637             ],
638             'accept_multiple' => [
639                 'suffix' => 'dl',
640                 'group' => 'deep_linking_settings',
641                 'claim' => 'accept_multiple',
642                 'isarray' => false
643             ],
644             'accept_presentation_document_targets' => [
645                 'suffix' => 'dl',
646                 'group' => 'deep_linking_settings',
647                 'claim' => 'accept_presentation_document_targets',
648                 'isarray' => true
649             ],
650             'accept_types' => [
651                 'suffix' => 'dl',
652                 'group' => 'deep_linking_settings',
653                 'claim' => 'accept_types',
654                 'isarray' => true
655             ],
656             'accept_unsigned' => [
657                 'suffix' => 'dl',
658                 'group' => 'deep_linking_settings',
659                 'claim' => 'accept_unsigned',
660                 'isarray' => false
661             ],
662             'auto_create' => [
663                 'suffix' => 'dl',
664                 'group' => 'deep_linking_settings',
665                 'claim' => 'auto_create',
666                 'isarray' => false
667             ],
668             'can_confirm' => [
669                 'suffix' => 'dl',
670                 'group' => 'deep_linking_settings',
671                 'claim' => 'can_confirm',
672                 'isarray' => false
673             ],
674             'content_item_return_url' => [
675                 'suffix' => 'dl',
676                 'group' => 'deep_linking_settings',
677                 'claim' => 'deep_link_return_url',
678                 'isarray' => false
679             ],
680             'content_items' => [
681                 'suffix' => 'dl',
682                 'group' => '',
683                 'claim' => 'content_items',
684                 'isarray' => true
685             ],
686             'data' => [
687                 'suffix' => 'dl',
688                 'group' => 'deep_linking_settings',
689                 'claim' => 'data',
690                 'isarray' => false
691             ],
692             'text' => [
693                 'suffix' => 'dl',
694                 'group' => 'deep_linking_settings',
695                 'claim' => 'text',
696                 'isarray' => false
697             ],
698             'title' => [
699                 'suffix' => 'dl',
700                 'group' => 'deep_linking_settings',
701                 'claim' => 'title',
702                 'isarray' => false
703             ],
704             'lti_msg' => [
705                 'suffix' => 'dl',
706                 'group' => '',
707                 'claim' => 'msg',
708                 'isarray' => false
709             ],
710             'lti_log' => [
711                 'suffix' => 'dl',
712                 'group' => '',
713                 'claim' => 'log',
714                 'isarray' => false
715             ],
716             'lti_errormsg' => [
717                 'suffix' => 'dl',
718                 'group' => '',
719                 'claim' => 'errormsg',
720                 'isarray' => false
721             ],
722             'lti_errorlog' => [
723                 'suffix' => 'dl',
724                 'group' => '',
725                 'claim' => 'errorlog',
726                 'isarray' => false
727             ],
728             'context_id' => [
729                 'suffix' => '',
730                 'group' => 'context',
731                 'claim' => 'id',
732                 'isarray' => false
733             ],
734             'context_label' => [
735                 'suffix' => '',
736                 'group' => 'context',
737                 'claim' => 'label',
738                 'isarray' => false
739             ],
740             'context_title' => [
741                 'suffix' => '',
742                 'group' => 'context',
743                 'claim' => 'title',
744                 'isarray' => false
745             ],
746             'context_type' => [
747                 'suffix' => '',
748                 'group' => 'context',
749                 'claim' => 'type',
750                 'isarray' => true
751             ],
752             'lis_course_offering_sourcedid' => [
753                 'suffix' => '',
754                 'group' => 'lis',
755                 'claim' => 'course_offering_sourcedid',
756                 'isarray' => false
757             ],
758             'lis_course_section_sourcedid' => [
759                 'suffix' => '',
760                 'group' => 'lis',
761                 'claim' => 'course_section_sourcedid',
762                 'isarray' => false
763             ],
764             'launch_presentation_css_url' => [
765                 'suffix' => '',
766                 'group' => 'launch_presentation',
767                 'claim' => 'css_url',
768                 'isarray' => false
769             ],
770             'launch_presentation_document_target' => [
771                 'suffix' => '',
772                 'group' => 'launch_presentation',
773                 'claim' => 'document_target',
774                 'isarray' => false
775             ],
776             'launch_presentation_height' => [
777                 'suffix' => '',
778                 'group' => 'launch_presentation',
779                 'claim' => 'height',
780                 'isarray' => false
781             ],
782             'launch_presentation_locale' => [
783                 'suffix' => '',
784                 'group' => 'launch_presentation',
785                 'claim' => 'locale',
786                 'isarray' => false
787             ],
788             'launch_presentation_return_url' => [
789                 'suffix' => '',
790                 'group' => 'launch_presentation',
791                 'claim' => 'return_url',
792                 'isarray' => false
793             ],
794             'launch_presentation_width' => [
795                 'suffix' => '',
796                 'group' => 'launch_presentation',
797                 'claim' => 'width',
798                 'isarray' => false
799             ],
800             'lis_person_contact_email_primary' => [
801                 'suffix' => '',
802                 'group' => null,
803                 'claim' => 'email',
804                 'isarray' => false
805             ],
806             'lis_person_name_family' => [
807                 'suffix' => '',
808                 'group' => null,
809                 'claim' => 'family_name',
810                 'isarray' => false
811             ],
812             'lis_person_name_full' => [
813                 'suffix' => '',
814                 'group' => null,
815                 'claim' => 'name',
816                 'isarray' => false
817             ],
818             'lis_person_name_given' => [
819                 'suffix' => '',
820                 'group' => null,
821                 'claim' => 'given_name',
822                 'isarray' => false
823             ],
824             'lis_person_sourcedid' => [
825                 'suffix' => '',
826                 'group' => 'lis',
827                 'claim' => 'person_sourcedid',
828                 'isarray' => false
829             ],
830             'user_id' => [
831                 'suffix' => '',
832                 'group' => null,
833                 'claim' => 'sub',
834                 'isarray' => false
835             ],
836             'user_image' => [
837                 'suffix' => '',
838                 'group' => null,
839                 'claim' => 'picture',
840                 'isarray' => false
841             ],
842             'roles' => [
843                 'suffix' => '',
844                 'group' => '',
845                 'claim' => 'roles',
846                 'isarray' => true
847             ],
848             'role_scope_mentor' => [
849                 'suffix' => '',
850                 'group' => '',
851                 'claim' => 'role_scope_mentor',
852                 'isarray' => false
853             ],
854             'deployment_id' => [
855                 'suffix' => '',
856                 'group' => '',
857                 'claim' => 'deployment_id',
858                 'isarray' => false
859             ],
860             'lti_message_type' => [
861                 'suffix' => '',
862                 'group' => '',
863                 'claim' => 'message_type',
864                 'isarray' => false
865             ],
866             'lti_version' => [
867                 'suffix' => '',
868                 'group' => '',
869                 'claim' => 'version',
870                 'isarray' => false
871             ],
872             'resource_link_description' => [
873                 'suffix' => '',
874                 'group' => 'resource_link',
875                 'claim' => 'description',
876                 'isarray' => false
877             ],
878             'resource_link_id' => [
879                 'suffix' => '',
880                 'group' => 'resource_link',
881                 'claim' => 'id',
882                 'isarray' => false
883             ],
884             'resource_link_title' => [
885                 'suffix' => '',
886                 'group' => 'resource_link',
887                 'claim' => 'title',
888                 'isarray' => false
889             ],
890             'tool_consumer_info_product_family_code' => [
891                 'suffix' => '',
892                 'group' => 'tool_platform',
893                 'claim' => 'family_code',
894                 'isarray' => false
895             ],
896             'tool_consumer_info_version' => [
897                 'suffix' => '',
898                 'group' => 'tool_platform',
899                 'claim' => 'version',
900                 'isarray' => false
901             ],
902             'tool_consumer_instance_contact_email' => [
903                 'suffix' => '',
904                 'group' => 'tool_platform',
905                 'claim' => 'contact_email',
906                 'isarray' => false
907             ],
908             'tool_consumer_instance_description' => [
909                 'suffix' => '',
910                 'group' => 'tool_platform',
911                 'claim' => 'description',
912                 'isarray' => false
913             ],
914             'tool_consumer_instance_guid' => [
915                 'suffix' => '',
916                 'group' => 'tool_platform',
917                 'claim' => 'guid',
918                 'isarray' => false
919             ],
920             'tool_consumer_instance_name' => [
921                 'suffix' => '',
922                 'group' => 'tool_platform',
923                 'claim' => 'name',
924                 'isarray' => false
925             ],
926             'tool_consumer_instance_url' => [
927                 'suffix' => '',
928                 'group' => 'tool_platform',
929                 'claim' => 'url',
930                 'isarray' => false
931             ],
932             'custom_context_memberships_url' => [
933                 'suffix' => 'nrps',
934                 'group' => 'namesroleservice',
935                 'claim' => 'context_memberships_url',
936                 'isarray' => false
937             ],
938             'custom_context_memberships_versions' => [
939                 'suffix' => 'nrps',
940                 'group' => 'namesroleservice',
941                 'claim' => 'service_versions',
942                 'isarray' => true
943             ],
944             'custom_gradebookservices_scope' => [
945                 'suffix' => 'ags',
946                 'group' => 'endpoint',
947                 'claim' => 'scope',
948                 'isarray' => true
949             ],
950             'custom_lineitems_url' => [
951                 'suffix' => 'ags',
952                 'group' => 'endpoint',
953                 'claim' => 'lineitems',
954                 'isarray' => false
955             ],
956             'custom_lineitem_url' => [
957                 'suffix' => 'ags',
958                 'group' => 'endpoint',
959                 'claim' => 'lineitem',
960                 'isarray' => false
961             ],
962             'custom_results_url' => [
963                 'suffix' => 'ags',
964                 'group' => 'endpoint',
965                 'claim' => 'results',
966                 'isarray' => false
967             ],
968             'custom_result_url' => [
969                 'suffix' => 'ags',
970                 'group' => 'endpoint',
971                 'claim' => 'result',
972                 'isarray' => false
973             ],
974             'custom_scores_url' => [
975                 'suffix' => 'ags',
976                 'group' => 'endpoint',
977                 'claim' => 'scores',
978                 'isarray' => false
979             ],
980             'custom_score_url' => [
981                 'suffix' => 'ags',
982                 'group' => 'endpoint',
983                 'claim' => 'score',
984                 'isarray' => false
985             ],
986             'lis_outcome_service_url' => [
987                 'suffix' => 'bos',
988                 'group' => 'basicoutcomesservice',
989                 'claim' => 'lis_outcome_service_url',
990                 'isarray' => false
991             ],
992             'lis_result_sourcedid' => [
993                 'suffix' => 'bos',
994                 'group' => 'basicoutcomesservice',
995                 'claim' => 'lis_result_sourcedid',
996                 'isarray' => false
997             ],
998         ];
1000         $this->assertEquals($mapping, lti_get_jwt_claim_mapping());
1001     }
1003     /**
1004      * Test lti_build_standard_message().
1005      */
1006     public function test_lti_build_standard_message_institution_name_set() {
1007         global $CFG;
1009         $this->resetAfterTest();
1011         $CFG->mod_lti_institution_name = 'some institution name lols';
1013         $course   = $this->getDataGenerator()->create_course();
1014         $instance = $this->getDataGenerator()->create_module('lti',
1015             [
1016                 'course' => $course->id,
1017             ]
1018         );
1020         $message = lti_build_standard_message($instance, '2', LTI_VERSION_1);
1022         $this->assertEquals('moodle-2', $message['ext_lms']);
1023         $this->assertEquals('moodle', $message['tool_consumer_info_product_family_code']);
1024         $this->assertEquals(LTI_VERSION_1, $message['lti_version']);
1025         $this->assertEquals('basic-lti-launch-request', $message['lti_message_type']);
1026         $this->assertEquals('2', $message['tool_consumer_instance_guid']);
1027         $this->assertEquals('some institution name lols', $message['tool_consumer_instance_name']);
1028         $this->assertEquals('PHPUnit test site', $message['tool_consumer_instance_description']);
1029     }
1031     /**
1032      * Test lti_build_standard_message().
1033      */
1034     public function test_lti_build_standard_message_institution_name_not_set() {
1035         $this->resetAfterTest();
1037         $course   = $this->getDataGenerator()->create_course();
1038         $instance = $this->getDataGenerator()->create_module('lti',
1039             [
1040                 'course' => $course->id,
1041             ]
1042         );
1044         $message = lti_build_standard_message($instance, '2', LTI_VERSION_2);
1046         $this->assertEquals('moodle-2', $message['ext_lms']);
1047         $this->assertEquals('moodle', $message['tool_consumer_info_product_family_code']);
1048         $this->assertEquals(LTI_VERSION_2, $message['lti_version']);
1049         $this->assertEquals('basic-lti-launch-request', $message['lti_message_type']);
1050         $this->assertEquals('2', $message['tool_consumer_instance_guid']);
1051         $this->assertEquals('phpunit', $message['tool_consumer_instance_name']);
1052         $this->assertEquals('PHPUnit test site', $message['tool_consumer_instance_description']);
1053     }
1055     /**
1056      * Test lti_verify_jwt_signature().
1057      */
1058     public function test_lti_verify_jwt_signature() {
1059         $this->resetAfterTest();
1061         $this->setAdminUser();
1063         // Create a tool type, associated with that proxy.
1064         $type = new stdClass();
1065         $type->state = LTI_TOOL_STATE_CONFIGURED;
1066         $type->name = "Test tool";
1067         $type->description = "Example description";
1068         $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1070         $config = new stdClass();
1071         $config->lti_publickey = '-----BEGIN PUBLIC KEY-----
1072 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv
1073 vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc
1074 aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy
1075 tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0
1076 e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb
1077 V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9
1078 MwIDAQAB
1079 -----END PUBLIC KEY-----';
1081         $config->lti_keytype = LTI_RSA_KEY;
1083         $typeid = lti_add_type($type, $config);
1085         lti_verify_jwt_signature($typeid, '', 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4g' .
1086             'RG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POstGetfAytaZS82wHcjoTyoqhMyxXiWdR7Nn7A29DNSl0EiXLdwJ6xC6AfgZWF1bOs' .
1087             'S_TuYI3OG85AmiExREkrS6tDfTQ2B3WXlrr-wp5AokiRbz3_oB4OxG-W9KcEEbDRcZc0nH3L7LzYptiy1PtAylQGxHTWZXtGz4ht0bAecBgmpdgXMgu' .
1088             'EIcoqPJ1n3pIWk_dUZegpqx0Lka21H6XxUTxiy8OcaarA8zdnPUnV6AmNP3ecFawIFYdvJB_cm-GvpCSbr8G8y_Mllj8f4x9nBH8pQux89_6gUY618iY' .
1089             'v7tuPWBFfEbLxtF2pZS6YC1aSfLQxeNe8djT9YjpvRZA');
1090     }
1092     /**
1093      * Test lti_verify_jwt_signature_jwk().
1094      */
1095     public function test_lti_verify_jwt_signature_jwk() {
1096         $this->resetAfterTest();
1098         $this->setAdminUser();
1100         // Create a tool type, associated with that proxy.
1101         $type = new stdClass();
1102         $type->state = LTI_TOOL_STATE_CONFIGURED;
1103         $type->name = "Test tool";
1104         $type->description = "Example description";
1105         $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1107         $config = new stdClass();
1108         $config->lti_publickeyset = dirname(__FILE__) . '/fixtures/test_keyset';
1110         $config->lti_keytype = LTI_JWK_KEYSET;
1112         $typeid = lti_add_type($type, $config);
1114         $jwt = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjU3YzExNzdkMmQ1M2EwMjFjNzM';
1115         $jwt .= '3NTY0OTFjMTM3YjE3In0.eyJpc3MiOiJnclJvbkd3RTd1WjRwZ28iLCJzdWIiOiJnclJvb';
1116         $jwt .= 'kd3RTd1WjRwZ28iLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0L21vb2RsZS9tb2QvbHRpL3R';
1117         $jwt .= 'va2VuLnBocCIsImp0aSI6IjFlMUJPVEczVFJjbFdUem00dERsMGc9PSIsImlhdCI6MTU4M';
1118         $jwt .= 'Dg1NTUwNX0.Lowhc9ovNAXRb2rkAnv1oozDXlRD54Mz2JS1i8Zx4yGWQzmXzam-La19_g0';
1119         $jwt .= 'CTnwlKM6gxaInnRKFRAcwhJVcWec389liLAjMbna6d6iTWYTZr7q_4BIe3CT_oTMWASGta';
1120         $jwt .= 'Paaq53ch1rO4YdueEtmtd1K47ibo4Lhu1jmP_icc3lxjfnqiv4vIYdy7W2JQEzpk1ImuQr';
1121         $jwt .= 'AlO1xR3fZ6bgcJhVIaw5xoaZD3ZgEjuZOQXMkywv1bL-mL17RX336CzHd8rYZg82QXrBzb';
1122         $jwt .= 'NWzAlaZxv9VSug8t6mORvM6TkYYWjqEBKemgkD5rNh1BHrPcjWP7vy2Jz7YMjLsmuvDuLK';
1123         $jwt .= '_PHYIKL--s4gcXWoYmOu1vj-SgoPczTJPoiBD35hAKqVHy5ggHaYHBy95_bbcFd8H1smHw';
1124         $jwt .= 'pejrAFj1QAwGyTISLzUm08oq7Ak0tSxRKKXw4lpZAka1MmYxO3tJ_3-MXw6Bwz12bNgitJ';
1125         $jwt .= 'lQd6n3kkGLCJAmANeRkPsH6eZVwF0n2cjh2O1JAwyNcMD2vs4I8ftM1EqqoE2M3r6kt3AC';
1126         $jwt .= 'EscmqzizI3j80USBCLUUb1UTsfJb2g7oyApJAp-13Q3InR3QyvWO8unG5VraFE7IL5I28h';
1127         $jwt .= 'MkQAHuCI90DFmXB4leflAu7wNlIK_U8xkGl8X8Mnv6MWgg94Ki8jgIq_kA85JAqI';
1129         lti_verify_jwt_signature($typeid, '', $jwt);
1130     }
1132     /**
1133      * Test lti_verify_jwt_signature().
1134      */
1135     public function test_lti_verify_jwt_signature_with_lti2() {
1136         $this->resetAfterTest();
1138         $this->setAdminUser();
1140         // Create a tool proxy.
1141         $proxy = mod_lti_external::create_tool_proxy('Test proxy', $this->getExternalTestFileUrl('/test.html'), array(), array());
1143         // Create a tool type, associated with that proxy.
1144         $type = new stdClass();
1145         $type->state = LTI_TOOL_STATE_CONFIGURED;
1146         $type->name = "Test tool";
1147         $type->description = "Example description";
1148         $type->toolproxyid = $proxy->id;
1149         $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1151         $data = new stdClass();
1152         $data->lti_contentitem = true;
1154         $typeid = lti_add_type($type, $data);
1156         $this->expectExceptionMessage('JWT security not supported with LTI 2');
1157         lti_verify_jwt_signature($typeid, '', '');
1158     }
1160     /**
1161      * Test lti_verify_jwt_signature().
1162      */
1163     public function test_lti_verify_jwt_signature_no_consumer_key() {
1164         $this->resetAfterTest();
1166         $this->setAdminUser();
1168         // Create a tool type, associated with that proxy.
1169         $type = new stdClass();
1170         $type->state = LTI_TOOL_STATE_CONFIGURED;
1171         $type->name = "Test tool";
1172         $type->description = "Example description";
1173         $type->clientid = 'consumerkey';
1174         $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1176         $config = new stdClass();
1177         $typeid = lti_add_type($type, $config);
1179         $this->expectExceptionMessage(get_string('errorincorrectconsumerkey', 'mod_lti'));
1180         lti_verify_jwt_signature($typeid, '', '');
1181     }
1183     /**
1184      * Test lti_verify_jwt_signature().
1185      */
1186     public function test_lti_verify_jwt_signature_no_public_key() {
1187         $this->resetAfterTest();
1189         $this->setAdminUser();
1191         // Create a tool type, associated with that proxy.
1192         $type = new stdClass();
1193         $type->state = LTI_TOOL_STATE_CONFIGURED;
1194         $type->name = "Test tool";
1195         $type->description = "Example description";
1196         $type->clientid = 'consumerkey';
1197         $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1199         $config = new stdClass();
1200         $config->lti_keytype = LTI_RSA_KEY;
1201         $typeid = lti_add_type($type, $config);
1203         $this->expectExceptionMessage('No public key configured');
1204         lti_verify_jwt_signature($typeid, 'consumerkey', '');
1205     }
1207     /**
1208      * Test lti_convert_content_items().
1209      */
1210     public function test_lti_convert_content_items() {
1211         $contentitems = [];
1212         $contentitems[] = [
1213             'type' => 'ltiResourceLink',
1214             'url' => 'http://example.com/messages/launch',
1215             'title' => 'Test title',
1216             'text' => 'Test text',
1217             'frame' => []
1218         ];
1220         $contentitems = json_encode($contentitems);
1222         $json = lti_convert_content_items($contentitems);
1224         $jsondecode = json_decode($json);
1226         $strcontext = '@context';
1227         $strgraph = '@graph';
1228         $strtype = '@type';
1230         $objgraph = new stdClass();
1231         $objgraph->url = 'http://example.com/messages/launch';
1232         $objgraph->title = 'Test title';
1233         $objgraph->text = 'Test text';
1234         $objgraph->frame = [];
1235         $objgraph->{$strtype} = 'LtiLinkItem';
1236         $objgraph->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
1238         $expected = new stdClass();
1239         $expected->{$strcontext} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem';
1240         $expected->{$strgraph} = [];
1241         $expected->{$strgraph}[] = $objgraph;
1243         $this->assertEquals($expected, $jsondecode);
1244     }
1246     /**
1247      * Test lti_sign_jwt().
1248      */
1249     public function test_lti_sign_jwt() {
1250         $this->resetAfterTest();
1252         $this->setAdminUser();
1254         // Create a tool type, associated with that proxy.
1255         $type = new stdClass();
1256         $type->state = LTI_TOOL_STATE_CONFIGURED;
1257         $type->name = "Test tool";
1258         $type->description = "Example description";
1259         $type->clientid = 'consumerkey';
1260         $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1262         $config = new stdClass();
1263         $typeid = lti_add_type($type, $config);
1265         $params = [];
1266         $params['roles'] = 'urn:lti:role:ims/lis/testrole,' .
1267             'urn:lti:instrole:ims/lis/testinstrole,' .
1268             'urn:lti:sysrole:ims/lis/testsysrole,' .
1269             'hi';
1270         $params['accept_copy_advice'] = [
1271             'suffix' => 'dl',
1272             'group' => 'deep_linking_settings',
1273             'claim' => 'accept_copy_advice',
1274             'isarray' => false
1275         ];
1276         $params['lis_result_sourcedid'] = [
1277             'suffix' => 'bos',
1278             'group' => 'basicoutcomesservice',
1279             'claim' => 'lis_result_sourcedid',
1280             'isarray' => false
1281         ];
1282         $endpoint = 'https://www.example.com/moodle';
1283         $oauthconsumerkey = 'consumerkey';
1284         $nonce = '';
1286         $jwt = lti_sign_jwt($params, $endpoint, $oauthconsumerkey, $typeid, $nonce);
1288         $this->assertArrayHasKey('id_token', $jwt);
1289         $this->assertNotEmpty($jwt['id_token']);
1290     }
1292     /**
1293      * Test lti_convert_from_jwt()
1294      */
1295     public function test_lti_convert_from_jwt() {
1296         $this->resetAfterTest();
1298         $this->setAdminUser();
1300         // Create a tool type, associated with that proxy.
1301         $type = new stdClass();
1302         $type->state = LTI_TOOL_STATE_CONFIGURED;
1303         $type->name = "Test tool";
1304         $type->description = "Example description";
1305         $type->clientid = 'sso.example.com';
1306         $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1308         $config = new stdClass();
1309         $config->lti_publickey = '-----BEGIN PUBLIC KEY-----
1310 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv
1311 vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc
1312 aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy
1313 tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0
1314 e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb
1315 V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9
1316 MwIDAQAB
1317 -----END PUBLIC KEY-----';
1318         $config->lti_keytype = LTI_RSA_KEY;
1320         $typeid = lti_add_type($type, $config);
1322         $params = lti_convert_from_jwt($typeid, 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwib' .
1323             'mFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiaXNzIjoic3NvLmV4YW1wbGUuY29tIn0.XURVvEb5ueAvFsn-S9EB' .
1324             'BSfKbsgUzfRQqmJ6evlrYdx7sXWoZXw1nYjaLTg-mawvBr7MVvrdG9qh6oN8OfkQ7bfMwiz4tjBMJ4B4q_sig5BDYIKwMNjZL5GGCBs89FQrgqZBhxw' .
1325             '3exTjPBEn69__w40o0AhCsBohPMh0ZsAyHug5dhm8vIuOP667repUJzM8uKCD6L4bEL6vQE8EwU6WQOmfJ2SDmRs-1pFkiaFd6hmPn6AVX7ETtzQmlT' .
1326             'X-nXe9weQjU1lH4AQG2Yfnn-7lS94bt6E76Zt-XndP3IY7W48EpnRfUK9Ff1fZlomT4MPahdNP1eP8gT2iMz7vYpCfmA');
1328         $this->assertEquals('sso.example.com', $params['oauth_consumer_key']);
1329         $this->assertEquals('John Doe', $params['lis_person_name_full']);
1330     }
1332     /**
1333      * Test lti_get_permitted_service_scopes().
1334      */
1335     public function test_lti_get_permitted_service_scopes() {
1336         $this->resetAfterTest();
1338         $this->setAdminUser();
1340         // Create a tool type, associated with that proxy.
1341         $type = new stdClass();
1342         $type->state = LTI_TOOL_STATE_CONFIGURED;
1343         $type->name = "Test tool";
1344         $type->description = "Example description";
1345         $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1347         $typeconfig = new stdClass();
1348         $typeconfig->lti_acceptgrades = true;
1350         $typeid = lti_add_type($type, $typeconfig);
1352         $tool = lti_get_type($typeid);
1354         $config = lti_get_type_config($typeid);
1355         $permittedscopes = lti_get_permitted_service_scopes($tool, $config);
1357         $expected = [
1358             'https://purl.imsglobal.org/spec/lti-bo/scope/basicoutcome'
1359         ];
1360         $this->assertEquals($expected, $permittedscopes);
1361     }
1363     /**
1364      * Test get_tool_type_config().
1365      */
1366     public function test_get_tool_type_config() {
1367         $this->resetAfterTest();
1369         $this->setAdminUser();
1371         // Create a tool type, associated with that proxy.
1372         $type = new stdClass();
1373         $type->state = LTI_TOOL_STATE_CONFIGURED;
1374         $type->name = "Test tool";
1375         $type->description = "Example description";
1376         $type->clientid = "Test client ID";
1377         $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1379         $config = new stdClass();
1381         $typeid = lti_add_type($type, $config);
1383         $type = lti_get_type($typeid);
1385         $typeconfig = get_tool_type_config($type);
1387         $this->assertEquals('https://www.example.com/moodle', $typeconfig['platformid']);
1388         $this->assertEquals($type->clientid, $typeconfig['clientid']);
1389         $this->assertEquals($typeid, $typeconfig['deploymentid']);
1390         $this->assertEquals('https://www.example.com/moodle/mod/lti/certs.php', $typeconfig['publickeyseturl']);
1391         $this->assertEquals('https://www.example.com/moodle/mod/lti/token.php', $typeconfig['accesstokenurl']);
1392         $this->assertEquals('https://www.example.com/moodle/mod/lti/auth.php', $typeconfig['authrequesturl']);
1393     }
1395     /**
1396      * Test lti_new_access_token().
1397      */
1398     public function test_lti_new_access_token() {
1399         global $DB;
1401         $this->resetAfterTest();
1403         $this->setAdminUser();
1405         // Create a tool type, associated with that proxy.
1406         $type = new stdClass();
1407         $type->state = LTI_TOOL_STATE_CONFIGURED;
1408         $type->name = "Test tool";
1409         $type->description = "Example description";
1410         $type->clientid = "Test client ID";
1411         $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1413         $config = new stdClass();
1415         $typeid = lti_add_type($type, $config);
1417         $scopes = ['lti_some_scope', 'lti_another_scope'];
1419         lti_new_access_token($typeid, $scopes);
1421         $token = $DB->get_records('lti_access_tokens');
1422         $this->assertEquals(1, count($token));
1424         $token = reset($token);
1426         $this->assertEquals($typeid, $token->typeid);
1427         $this->assertEquals(json_encode(array_values($scopes)), $token->scope);
1428         $this->assertEquals($token->timecreated + LTI_ACCESS_TOKEN_LIFE, $token->validuntil);
1429         $this->assertNull($token->lastaccess);
1430     }
1432     /**
1433      * Test lti_build_login_request().
1434      */
1435     public function test_lti_build_login_request() {
1436         global $USER, $CFG;
1438         $this->resetAfterTest();
1440         $USER->id = 123456789;
1442         $course   = $this->getDataGenerator()->create_course();
1443         $instance = $this->getDataGenerator()->create_module('lti',
1444             [
1445                 'course' => $course->id,
1446             ]
1447         );
1449         $config = new stdClass();
1450         $config->lti_clientid = 'some-client-id';
1451         $config->typeid = 'some-type-id';
1452         $config->lti_toolurl = 'some-lti-tool-url';
1454         $request = lti_build_login_request($course->id, $instance->id, $instance, $config, 'basic-lti-launch-request');
1456         $this->assertEquals($CFG->wwwroot, $request['iss']);
1457         $this->assertEquals('http://some-lti-tool-url', $request['target_link_uri']);
1458         $this->assertEquals(123456789, $request['login_hint']);
1459         $this->assertEquals($instance->id, $request['lti_message_hint']);
1460         $this->assertEquals('some-client-id', $request['client_id']);
1461         $this->assertEquals('some-type-id', $request['lti_deployment_id']);
1462     }
1464     /**
1465      * Test default orgid is host if not specified in config (tool installed in earlier version of Moodle).
1466      */
1467     public function test_lti_get_launch_data_default_organizationid_unset_usehost() {
1468         global $DB;
1469         $this->resetAfterTest();
1470         $this->setAdminUser();
1471         $config = new stdClass();
1472         $config->lti_organizationid = '';
1473         $course = $this->getDataGenerator()->create_course();
1474         $type = $this->create_type($config);
1475         $link = $this->create_instance($type, $course);
1476         $launchdata = lti_get_launch_data($link);
1477         $this->assertEquals($launchdata[1]['tool_consumer_instance_guid'], 'www.example.com');
1478     }
1480     /**
1481      * Test default org id is set to host when config is usehost.
1482      */
1483     public function test_lti_get_launch_data_default_organizationid_set_usehost() {
1484         global $DB;
1485         $this->resetAfterTest();
1486         $this->setAdminUser();
1487         $config = new stdClass();
1488         $config->lti_organizationid = '';
1489         $config->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEHOST;
1490         $course = $this->getDataGenerator()->create_course();
1491         $type = $this->create_type($config);
1492         $link = $this->create_instance($type, $course);
1493         $launchdata = lti_get_launch_data($link);
1494         $this->assertEquals($launchdata[1]['tool_consumer_instance_guid'], 'www.example.com');
1495     }
1497     /**
1498      * Test default org id is set to site id when config is usesiteid.
1499      */
1500     public function test_lti_get_launch_data_default_organizationid_set_usesiteid() {
1501         global $DB;
1502         $this->resetAfterTest();
1503         $this->setAdminUser();
1504         $config = new stdClass();
1505         $config->lti_organizationid = '';
1506         $config->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEID;
1507         $course = $this->getDataGenerator()->create_course();
1508         $type = $this->create_type($config);
1509         $link = $this->create_instance($type, $course);
1510         $launchdata = lti_get_launch_data($link);
1511         $this->assertEquals($launchdata[1]['tool_consumer_instance_guid'], md5(get_site_identifier()));
1512     }
1514     /**
1515      * Test orgid can be overridden in which case default is ignored.
1516      */
1517     public function test_lti_get_launch_data_default_organizationid_orgid_override() {
1518         global $DB;
1519         $this->resetAfterTest();
1520         $this->setAdminUser();
1521         $config = new stdClass();
1522         $config->lti_organizationid = 'overridden!';
1523         $config->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEID;
1524         $course = $this->getDataGenerator()->create_course();
1525         $type = $this->create_type($config);
1526         $link = $this->create_instance($type, $course);
1527         $launchdata = lti_get_launch_data($link);
1528         $this->assertEquals($launchdata[1]['tool_consumer_instance_guid'], 'overridden!');
1529     }
1531     /**
1532      * Create an LTI Tool.
1533      *
1534      * @param object $config tool config.
1535      *
1536      * @return object tool.
1537      */
1538     private function create_type(object $config) {
1539         $type = new stdClass();
1540         $type->state = LTI_TOOL_STATE_CONFIGURED;
1541         $type->name = "Test tool";
1542         $type->description = "Example description";
1543         $type->clientid = "Test client ID";
1544         $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1546         $configbase = new stdClass();
1547         $configbase->lti_acceptgrades = LTI_SETTING_NEVER;
1548         $configbase->lti_sendname = LTI_SETTING_NEVER;
1549         $configbase->lti_sendemailaddr = LTI_SETTING_NEVER;
1550         $mergedconfig = (object) array_merge( (array) $configbase, (array) $config);
1551         $typeid = lti_add_type($type, $mergedconfig);
1552         return lti_get_type($typeid);
1553     }
1555     /**
1556      * Create an LTI Instance for the tool in a given course.
1557      *
1558      * @param object $type tool for which an instance should be added.
1559      * @param object $course course where the instance should be added.
1560      *
1561      * @return object instance.
1562      */
1563     private function create_instance(object $type, object $course) {
1564         $generator = $this->getDataGenerator()->get_plugin_generator('mod_lti');
1565         return $generator->create_instance(array('course' => $course->id,
1566                   'toolurl' => $type->baseurl,
1567                   'typeid' => $type->id
1568                   ), array());
1569     }