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 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 */
49
50defined('MOODLE_INTERNAL') || die;
51
52global $CFG;
53require_once($CFG->dirroot . '/mod/lti/locallib.php');
54require_once($CFG->dirroot . '/mod/lti/servicelib.php');
55
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 */
63class mod_lti_locallib_testcase extends advanced_testcase {
64
65    public function test_split_custom_parameters() {
66        $this->resetAfterTest();
67
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'));
74
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'));
78
79        // Removed repeat of previous test with a semicolon separator.
80
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'));
85
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'));
91
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    }
98
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');
113
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');
116
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']));
122
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    }
131
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';
167
168        $parsed = lti_parse_grade_replace_message(new SimpleXMLElement($message));
169
170        $this->assertEquals($parsed->userid, '2');
171        $this->assertEquals($parsed->instanceid, '2');
172        $this->assertEquals($parsed->sourcedidhash, '0b5078feab59b9938c333ceaae21d8e003a7b295e43cdf55338445254421076b');
173
174        $ltiinstance = (object)array('servicesalt' => '4e5fcc06de1d58.44963230');
175
176        lti_verify_sourcedid($ltiinstance, $parsed);
177    }
178
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    }
184
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    }
200
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();
206
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        ));
214
215        $typeconfig = array(
216            'acceptgrades'     => 1,
217            'forcessl'         => 0,
218            'sendname'         => 2,
219            'sendemailaddr'    => 2,
220            'customparameters' => '',
221        );
222
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']);
226
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']);
231
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    }
238
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();
245
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        ));
253
254        $typeconfig = array(
255            'acceptgrades'     => 1,
256            'forcessl'         => 0,
257            'sendname'         => 2,
258            'sendemailaddr'    => 2,
259            'customparameters' => '',
260        );
261
262        $params = lti_build_request($instance, $typeconfig, $course, null);
263
264        $ncount = substr_count($params['resource_link_description'], "\n");
265        $this->assertGreaterThan(0, $ncount);
266
267        $rcount = substr_count($params['resource_link_description'], "\r");
268        $this->assertGreaterThan(0, $rcount);
269
270        $this->assertEquals($ncount, $rcount, 'The number of \n characters should be the same as the number of \r characters');
271
272        $rncount = substr_count($params['resource_link_description'], "\r\n");
273        $this->assertGreaterThan(0, $rncount);
274
275        $this->assertEquals($ncount, $rncount, 'All newline characters should be a combination of \r\n');
276    }
277
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();
284
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);
290
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);
297
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    }
305
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');
312
313        lti_load_type_if_cartridge($type);
314
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    }
321
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');
328
329        lti_load_tool_if_cartridge($lti);
330
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    }
338
339    /**
340     * Tests for lti_build_content_item_selection_request().
341     */
342    public function test_lti_build_content_item_selection_request() {
343        $this->resetAfterTest();
344
345        $this->setAdminUser();
346        // Create a tool proxy.
347        $proxy = mod_lti_external::create_tool_proxy('Test proxy', $this->getExternalTestFileUrl('/test.html'), array(), array());
348
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');
358
359        $typeid = lti_add_type($type, $data);
360
361        $typeconfig = lti_get_type_config($typeid);
362
363        $course = $this->getDataGenerator()->create_course();
364        $returnurl = new moodle_url('/');
365
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');
390
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']);
410
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    }
427
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();
433
434        $this->setAdminUser();
435        $course = $this->getDataGenerator()->create_course();
436        $returnurl = new moodle_url('/');
437
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    }
442
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();
448
449        $this->setAdminUser();
450
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');
459
460        $typeid = lti_add_type($type, $data);
461        $course = $this->getDataGenerator()->create_course();
462        $returnurl = new moodle_url('/');
463
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    }
469
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();
475
476        $this->setAdminUser();
477
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');
486
487        $typeid = lti_add_type($type, $data);
488        $course = $this->getDataGenerator()->create_course();
489        $returnurl = new moodle_url('/');
490
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    }
496
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        ];
547
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        ];
586
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    }
594
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    }
607
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        ];
617
618        $this->assertEquals($mapping, lti_get_jwt_message_type_mapping());
619    }
620
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                'type' => 'boolean'
632            ],
633            'accept_media_types' => [
634                'suffix' => 'dl',
635                'group' => 'deep_linking_settings',
636                'claim' => 'accept_media_types',
637                'isarray' => true
638            ],
639            'accept_multiple' => [
640                'suffix' => 'dl',
641                'group' => 'deep_linking_settings',
642                'claim' => 'accept_multiple',
643                'isarray' => false,
644                'type' => 'boolean'
645            ],
646            'accept_presentation_document_targets' => [
647                'suffix' => 'dl',
648                'group' => 'deep_linking_settings',
649                'claim' => 'accept_presentation_document_targets',
650                'isarray' => true
651            ],
652            'accept_types' => [
653                'suffix' => 'dl',
654                'group' => 'deep_linking_settings',
655                'claim' => 'accept_types',
656                'isarray' => true
657            ],
658            'accept_unsigned' => [
659                'suffix' => 'dl',
660                'group' => 'deep_linking_settings',
661                'claim' => 'accept_unsigned',
662                'isarray' => false,
663                'type' => 'boolean'
664            ],
665            'auto_create' => [
666                'suffix' => 'dl',
667                'group' => 'deep_linking_settings',
668                'claim' => 'auto_create',
669                'isarray' => false,
670                'type' => 'boolean'
671            ],
672            'can_confirm' => [
673                'suffix' => 'dl',
674                'group' => 'deep_linking_settings',
675                'claim' => 'can_confirm',
676                'isarray' => false,
677                'type' => 'boolean'
678            ],
679            'content_item_return_url' => [
680                'suffix' => 'dl',
681                'group' => 'deep_linking_settings',
682                'claim' => 'deep_link_return_url',
683                'isarray' => false
684            ],
685            'content_items' => [
686                'suffix' => 'dl',
687                'group' => '',
688                'claim' => 'content_items',
689                'isarray' => true
690            ],
691            'data' => [
692                'suffix' => 'dl',
693                'group' => 'deep_linking_settings',
694                'claim' => 'data',
695                'isarray' => false
696            ],
697            'text' => [
698                'suffix' => 'dl',
699                'group' => 'deep_linking_settings',
700                'claim' => 'text',
701                'isarray' => false
702            ],
703            'title' => [
704                'suffix' => 'dl',
705                'group' => 'deep_linking_settings',
706                'claim' => 'title',
707                'isarray' => false
708            ],
709            'lti_msg' => [
710                'suffix' => 'dl',
711                'group' => '',
712                'claim' => 'msg',
713                'isarray' => false
714            ],
715            'lti_log' => [
716                'suffix' => 'dl',
717                'group' => '',
718                'claim' => 'log',
719                'isarray' => false
720            ],
721            'lti_errormsg' => [
722                'suffix' => 'dl',
723                'group' => '',
724                'claim' => 'errormsg',
725                'isarray' => false
726            ],
727            'lti_errorlog' => [
728                'suffix' => 'dl',
729                'group' => '',
730                'claim' => 'errorlog',
731                'isarray' => false
732            ],
733            'context_id' => [
734                'suffix' => '',
735                'group' => 'context',
736                'claim' => 'id',
737                'isarray' => false
738            ],
739            'context_label' => [
740                'suffix' => '',
741                'group' => 'context',
742                'claim' => 'label',
743                'isarray' => false
744            ],
745            'context_title' => [
746                'suffix' => '',
747                'group' => 'context',
748                'claim' => 'title',
749                'isarray' => false
750            ],
751            'context_type' => [
752                'suffix' => '',
753                'group' => 'context',
754                'claim' => 'type',
755                'isarray' => true
756            ],
757            'lis_course_offering_sourcedid' => [
758                'suffix' => '',
759                'group' => 'lis',
760                'claim' => 'course_offering_sourcedid',
761                'isarray' => false
762            ],
763            'lis_course_section_sourcedid' => [
764                'suffix' => '',
765                'group' => 'lis',
766                'claim' => 'course_section_sourcedid',
767                'isarray' => false
768            ],
769            'launch_presentation_css_url' => [
770                'suffix' => '',
771                'group' => 'launch_presentation',
772                'claim' => 'css_url',
773                'isarray' => false
774            ],
775            'launch_presentation_document_target' => [
776                'suffix' => '',
777                'group' => 'launch_presentation',
778                'claim' => 'document_target',
779                'isarray' => false
780            ],
781            'launch_presentation_height' => [
782                'suffix' => '',
783                'group' => 'launch_presentation',
784                'claim' => 'height',
785                'isarray' => false
786            ],
787            'launch_presentation_locale' => [
788                'suffix' => '',
789                'group' => 'launch_presentation',
790                'claim' => 'locale',
791                'isarray' => false
792            ],
793            'launch_presentation_return_url' => [
794                'suffix' => '',
795                'group' => 'launch_presentation',
796                'claim' => 'return_url',
797                'isarray' => false
798            ],
799            'launch_presentation_width' => [
800                'suffix' => '',
801                'group' => 'launch_presentation',
802                'claim' => 'width',
803                'isarray' => false
804            ],
805            'lis_person_contact_email_primary' => [
806                'suffix' => '',
807                'group' => null,
808                'claim' => 'email',
809                'isarray' => false
810            ],
811            'lis_person_name_family' => [
812                'suffix' => '',
813                'group' => null,
814                'claim' => 'family_name',
815                'isarray' => false
816            ],
817            'lis_person_name_full' => [
818                'suffix' => '',
819                'group' => null,
820                'claim' => 'name',
821                'isarray' => false
822            ],
823            'lis_person_name_given' => [
824                'suffix' => '',
825                'group' => null,
826                'claim' => 'given_name',
827                'isarray' => false
828            ],
829            'lis_person_sourcedid' => [
830                'suffix' => '',
831                'group' => 'lis',
832                'claim' => 'person_sourcedid',
833                'isarray' => false
834            ],
835            'user_id' => [
836                'suffix' => '',
837                'group' => null,
838                'claim' => 'sub',
839                'isarray' => false
840            ],
841            'user_image' => [
842                'suffix' => '',
843                'group' => null,
844                'claim' => 'picture',
845                'isarray' => false
846            ],
847            'roles' => [
848                'suffix' => '',
849                'group' => '',
850                'claim' => 'roles',
851                'isarray' => true
852            ],
853            'role_scope_mentor' => [
854                'suffix' => '',
855                'group' => '',
856                'claim' => 'role_scope_mentor',
857                'isarray' => false
858            ],
859            'deployment_id' => [
860                'suffix' => '',
861                'group' => '',
862                'claim' => 'deployment_id',
863                'isarray' => false
864            ],
865            'lti_message_type' => [
866                'suffix' => '',
867                'group' => '',
868                'claim' => 'message_type',
869                'isarray' => false
870            ],
871            'lti_version' => [
872                'suffix' => '',
873                'group' => '',
874                'claim' => 'version',
875                'isarray' => false
876            ],
877            'resource_link_description' => [
878                'suffix' => '',
879                'group' => 'resource_link',
880                'claim' => 'description',
881                'isarray' => false
882            ],
883            'resource_link_id' => [
884                'suffix' => '',
885                'group' => 'resource_link',
886                'claim' => 'id',
887                'isarray' => false
888            ],
889            'resource_link_title' => [
890                'suffix' => '',
891                'group' => 'resource_link',
892                'claim' => 'title',
893                'isarray' => false
894            ],
895            'tool_consumer_info_product_family_code' => [
896                'suffix' => '',
897                'group' => 'tool_platform',
898                'claim' => 'product_family_code',
899                'isarray' => false
900            ],
901            'tool_consumer_info_version' => [
902                'suffix' => '',
903                'group' => 'tool_platform',
904                'claim' => 'version',
905                'isarray' => false
906            ],
907            'tool_consumer_instance_contact_email' => [
908                'suffix' => '',
909                'group' => 'tool_platform',
910                'claim' => 'contact_email',
911                'isarray' => false
912            ],
913            'tool_consumer_instance_description' => [
914                'suffix' => '',
915                'group' => 'tool_platform',
916                'claim' => 'description',
917                'isarray' => false
918            ],
919            'tool_consumer_instance_guid' => [
920                'suffix' => '',
921                'group' => 'tool_platform',
922                'claim' => 'guid',
923                'isarray' => false
924            ],
925            'tool_consumer_instance_name' => [
926                'suffix' => '',
927                'group' => 'tool_platform',
928                'claim' => 'name',
929                'isarray' => false
930            ],
931            'tool_consumer_instance_url' => [
932                'suffix' => '',
933                'group' => 'tool_platform',
934                'claim' => 'url',
935                'isarray' => false
936            ],
937            'custom_context_memberships_url' => [
938                'suffix' => 'nrps',
939                'group' => 'namesroleservice',
940                'claim' => 'context_memberships_url',
941                'isarray' => false
942            ],
943            'custom_context_memberships_versions' => [
944                'suffix' => 'nrps',
945                'group' => 'namesroleservice',
946                'claim' => 'service_versions',
947                'isarray' => true
948            ],
949            'custom_gradebookservices_scope' => [
950                'suffix' => 'ags',
951                'group' => 'endpoint',
952                'claim' => 'scope',
953                'isarray' => true
954            ],
955            'custom_lineitems_url' => [
956                'suffix' => 'ags',
957                'group' => 'endpoint',
958                'claim' => 'lineitems',
959                'isarray' => false
960            ],
961            'custom_lineitem_url' => [
962                'suffix' => 'ags',
963                'group' => 'endpoint',
964                'claim' => 'lineitem',
965                'isarray' => false
966            ],
967            'custom_results_url' => [
968                'suffix' => 'ags',
969                'group' => 'endpoint',
970                'claim' => 'results',
971                'isarray' => false
972            ],
973            'custom_result_url' => [
974                'suffix' => 'ags',
975                'group' => 'endpoint',
976                'claim' => 'result',
977                'isarray' => false
978            ],
979            'custom_scores_url' => [
980                'suffix' => 'ags',
981                'group' => 'endpoint',
982                'claim' => 'scores',
983                'isarray' => false
984            ],
985            'custom_score_url' => [
986                'suffix' => 'ags',
987                'group' => 'endpoint',
988                'claim' => 'score',
989                'isarray' => false
990            ],
991            'lis_outcome_service_url' => [
992                'suffix' => 'bo',
993                'group' => 'basicoutcome',
994                'claim' => 'lis_outcome_service_url',
995                'isarray' => false
996            ],
997            'lis_result_sourcedid' => [
998                'suffix' => 'bo',
999                'group' => 'basicoutcome',
1000                'claim' => 'lis_result_sourcedid',
1001                'isarray' => false
1002            ],
1003        ];
1004
1005        $this->assertEquals($mapping, lti_get_jwt_claim_mapping());
1006    }
1007
1008    /**
1009     * Test lti_build_standard_message().
1010     */
1011    public function test_lti_build_standard_message_institution_name_set() {
1012        global $CFG;
1013
1014        $this->resetAfterTest();
1015
1016        $CFG->mod_lti_institution_name = 'some institution name lols';
1017
1018        $course   = $this->getDataGenerator()->create_course();
1019        $instance = $this->getDataGenerator()->create_module('lti',
1020            [
1021                'course' => $course->id,
1022            ]
1023        );
1024
1025        $message = lti_build_standard_message($instance, '2', LTI_VERSION_1);
1026
1027        $this->assertEquals('moodle-2', $message['ext_lms']);
1028        $this->assertEquals('moodle', $message['tool_consumer_info_product_family_code']);
1029        $this->assertEquals(LTI_VERSION_1, $message['lti_version']);
1030        $this->assertEquals('basic-lti-launch-request', $message['lti_message_type']);
1031        $this->assertEquals('2', $message['tool_consumer_instance_guid']);
1032        $this->assertEquals('some institution name lols', $message['tool_consumer_instance_name']);
1033        $this->assertEquals('PHPUnit test site', $message['tool_consumer_instance_description']);
1034    }
1035
1036    /**
1037     * Test lti_build_standard_message().
1038     */
1039    public function test_lti_build_standard_message_institution_name_not_set() {
1040        $this->resetAfterTest();
1041
1042        $course   = $this->getDataGenerator()->create_course();
1043        $instance = $this->getDataGenerator()->create_module('lti',
1044            [
1045                'course' => $course->id,
1046            ]
1047        );
1048
1049        $message = lti_build_standard_message($instance, '2', LTI_VERSION_2);
1050
1051        $this->assertEquals('moodle-2', $message['ext_lms']);
1052        $this->assertEquals('moodle', $message['tool_consumer_info_product_family_code']);
1053        $this->assertEquals(LTI_VERSION_2, $message['lti_version']);
1054        $this->assertEquals('basic-lti-launch-request', $message['lti_message_type']);
1055        $this->assertEquals('2', $message['tool_consumer_instance_guid']);
1056        $this->assertEquals('phpunit', $message['tool_consumer_instance_name']);
1057        $this->assertEquals('PHPUnit test site', $message['tool_consumer_instance_description']);
1058    }
1059
1060    /**
1061     * Test lti_verify_jwt_signature().
1062     */
1063    public function test_lti_verify_jwt_signature() {
1064        $this->resetAfterTest();
1065
1066        $this->setAdminUser();
1067
1068        // Create a tool type, associated with that proxy.
1069        $type = new stdClass();
1070        $type->state = LTI_TOOL_STATE_CONFIGURED;
1071        $type->name = "Test tool";
1072        $type->description = "Example description";
1073        $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1074
1075        $config = new stdClass();
1076        $config->lti_publickey = '-----BEGIN PUBLIC KEY-----
1077MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv
1078vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc
1079aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy
1080tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0
1081e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb
1082V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9
1083MwIDAQAB
1084-----END PUBLIC KEY-----';
1085
1086        $config->lti_keytype = LTI_RSA_KEY;
1087
1088        $typeid = lti_add_type($type, $config);
1089
1090        lti_verify_jwt_signature($typeid, '', 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4g' .
1091            'RG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POstGetfAytaZS82wHcjoTyoqhMyxXiWdR7Nn7A29DNSl0EiXLdwJ6xC6AfgZWF1bOs' .
1092            'S_TuYI3OG85AmiExREkrS6tDfTQ2B3WXlrr-wp5AokiRbz3_oB4OxG-W9KcEEbDRcZc0nH3L7LzYptiy1PtAylQGxHTWZXtGz4ht0bAecBgmpdgXMgu' .
1093            'EIcoqPJ1n3pIWk_dUZegpqx0Lka21H6XxUTxiy8OcaarA8zdnPUnV6AmNP3ecFawIFYdvJB_cm-GvpCSbr8G8y_Mllj8f4x9nBH8pQux89_6gUY618iY' .
1094            'v7tuPWBFfEbLxtF2pZS6YC1aSfLQxeNe8djT9YjpvRZA');
1095    }
1096
1097    /**
1098     * Test lti_verify_jwt_signature_jwk().
1099     */
1100    public function test_lti_verify_jwt_signature_jwk() {
1101        $this->resetAfterTest();
1102
1103        $this->setAdminUser();
1104
1105        // Create a tool type, associated with that proxy.
1106        $type = new stdClass();
1107        $type->state = LTI_TOOL_STATE_CONFIGURED;
1108        $type->name = "Test tool";
1109        $type->description = "Example description";
1110        $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1111
1112        $config = new stdClass();
1113        $config->lti_publickeyset = dirname(__FILE__) . '/fixtures/test_keyset';
1114
1115        $config->lti_keytype = LTI_JWK_KEYSET;
1116
1117        $typeid = lti_add_type($type, $config);
1118
1119        $jwt = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjU3YzExNzdkMmQ1M2EwMjFjNzM';
1120        $jwt .= '3NTY0OTFjMTM3YjE3In0.eyJpc3MiOiJnclJvbkd3RTd1WjRwZ28iLCJzdWIiOiJnclJvb';
1121        $jwt .= 'kd3RTd1WjRwZ28iLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0L21vb2RsZS9tb2QvbHRpL3R';
1122        $jwt .= 'va2VuLnBocCIsImp0aSI6IjFlMUJPVEczVFJjbFdUem00dERsMGc9PSIsImlhdCI6MTU4M';
1123        $jwt .= 'Dg1NTUwNX0.Lowhc9ovNAXRb2rkAnv1oozDXlRD54Mz2JS1i8Zx4yGWQzmXzam-La19_g0';
1124        $jwt .= 'CTnwlKM6gxaInnRKFRAcwhJVcWec389liLAjMbna6d6iTWYTZr7q_4BIe3CT_oTMWASGta';
1125        $jwt .= 'Paaq53ch1rO4YdueEtmtd1K47ibo4Lhu1jmP_icc3lxjfnqiv4vIYdy7W2JQEzpk1ImuQr';
1126        $jwt .= 'AlO1xR3fZ6bgcJhVIaw5xoaZD3ZgEjuZOQXMkywv1bL-mL17RX336CzHd8rYZg82QXrBzb';
1127        $jwt .= 'NWzAlaZxv9VSug8t6mORvM6TkYYWjqEBKemgkD5rNh1BHrPcjWP7vy2Jz7YMjLsmuvDuLK';
1128        $jwt .= '_PHYIKL--s4gcXWoYmOu1vj-SgoPczTJPoiBD35hAKqVHy5ggHaYHBy95_bbcFd8H1smHw';
1129        $jwt .= 'pejrAFj1QAwGyTISLzUm08oq7Ak0tSxRKKXw4lpZAka1MmYxO3tJ_3-MXw6Bwz12bNgitJ';
1130        $jwt .= 'lQd6n3kkGLCJAmANeRkPsH6eZVwF0n2cjh2O1JAwyNcMD2vs4I8ftM1EqqoE2M3r6kt3AC';
1131        $jwt .= 'EscmqzizI3j80USBCLUUb1UTsfJb2g7oyApJAp-13Q3InR3QyvWO8unG5VraFE7IL5I28h';
1132        $jwt .= 'MkQAHuCI90DFmXB4leflAu7wNlIK_U8xkGl8X8Mnv6MWgg94Ki8jgIq_kA85JAqI';
1133
1134        lti_verify_jwt_signature($typeid, '', $jwt);
1135    }
1136
1137    /**
1138     * Test lti_verify_jwt_signature().
1139     */
1140    public function test_lti_verify_jwt_signature_with_lti2() {
1141        $this->resetAfterTest();
1142
1143        $this->setAdminUser();
1144
1145        // Create a tool proxy.
1146        $proxy = mod_lti_external::create_tool_proxy('Test proxy', $this->getExternalTestFileUrl('/test.html'), array(), array());
1147
1148        // Create a tool type, associated with that proxy.
1149        $type = new stdClass();
1150        $type->state = LTI_TOOL_STATE_CONFIGURED;
1151        $type->name = "Test tool";
1152        $type->description = "Example description";
1153        $type->toolproxyid = $proxy->id;
1154        $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1155
1156        $data = new stdClass();
1157        $data->lti_contentitem = true;
1158
1159        $typeid = lti_add_type($type, $data);
1160
1161        $this->expectExceptionMessage('JWT security not supported with LTI 2');
1162        lti_verify_jwt_signature($typeid, '', '');
1163    }
1164
1165    /**
1166     * Test lti_verify_jwt_signature().
1167     */
1168    public function test_lti_verify_jwt_signature_no_consumer_key() {
1169        $this->resetAfterTest();
1170
1171        $this->setAdminUser();
1172
1173        // Create a tool type, associated with that proxy.
1174        $type = new stdClass();
1175        $type->state = LTI_TOOL_STATE_CONFIGURED;
1176        $type->name = "Test tool";
1177        $type->description = "Example description";
1178        $type->clientid = 'consumerkey';
1179        $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1180
1181        $config = new stdClass();
1182        $typeid = lti_add_type($type, $config);
1183
1184        $this->expectExceptionMessage(get_string('errorincorrectconsumerkey', 'mod_lti'));
1185        lti_verify_jwt_signature($typeid, '', '');
1186    }
1187
1188    /**
1189     * Test lti_verify_jwt_signature().
1190     */
1191    public function test_lti_verify_jwt_signature_no_public_key() {
1192        $this->resetAfterTest();
1193
1194        $this->setAdminUser();
1195
1196        // Create a tool type, associated with that proxy.
1197        $type = new stdClass();
1198        $type->state = LTI_TOOL_STATE_CONFIGURED;
1199        $type->name = "Test tool";
1200        $type->description = "Example description";
1201        $type->clientid = 'consumerkey';
1202        $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1203
1204        $config = new stdClass();
1205        $config->lti_keytype = LTI_RSA_KEY;
1206        $typeid = lti_add_type($type, $config);
1207
1208        $this->expectExceptionMessage('No public key configured');
1209        lti_verify_jwt_signature($typeid, 'consumerkey', '');
1210    }
1211
1212    /**
1213     * Test lti_convert_content_items().
1214     */
1215    public function test_lti_convert_content_items() {
1216        $contentitems = [];
1217        $contentitems[] = [
1218            'type' => 'ltiResourceLink',
1219            'url' => 'http://example.com/messages/launch',
1220            'title' => 'Test title',
1221            'text' => 'Test text',
1222            'frame' => []
1223        ];
1224
1225        $contentitems = json_encode($contentitems);
1226
1227        $json = lti_convert_content_items($contentitems);
1228
1229        $jsondecode = json_decode($json);
1230
1231        $strcontext = '@context';
1232        $strgraph = '@graph';
1233        $strtype = '@type';
1234
1235        $objgraph = new stdClass();
1236        $objgraph->url = 'http://example.com/messages/launch';
1237        $objgraph->title = 'Test title';
1238        $objgraph->text = 'Test text';
1239        $objgraph->frame = [];
1240        $objgraph->{$strtype} = 'LtiLinkItem';
1241        $objgraph->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
1242
1243        $expected = new stdClass();
1244        $expected->{$strcontext} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem';
1245        $expected->{$strgraph} = [];
1246        $expected->{$strgraph}[] = $objgraph;
1247
1248        $this->assertEquals($expected, $jsondecode);
1249    }
1250
1251    /**
1252     * Test lti_sign_jwt().
1253     */
1254    public function test_lti_sign_jwt() {
1255        $this->resetAfterTest();
1256
1257        $this->setAdminUser();
1258
1259        // Create a tool type, associated with that proxy.
1260        $type = new stdClass();
1261        $type->state = LTI_TOOL_STATE_CONFIGURED;
1262        $type->name = "Test tool";
1263        $type->description = "Example description";
1264        $type->clientid = 'consumerkey';
1265        $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1266
1267        $config = new stdClass();
1268        $typeid = lti_add_type($type, $config);
1269
1270        $params = [];
1271        $params['roles'] = 'urn:lti:role:ims/lis/testrole,' .
1272            'urn:lti:instrole:ims/lis/testinstrole,' .
1273            'urn:lti:sysrole:ims/lis/testsysrole,' .
1274            'hi';
1275        $params['accept_copy_advice'] = [
1276            'suffix' => 'dl',
1277            'group' => 'deep_linking_settings',
1278            'claim' => 'accept_copy_advice',
1279            'isarray' => false
1280        ];
1281        $params['lis_result_sourcedid'] = [
1282            'suffix' => 'bos',
1283            'group' => 'basicoutcomesservice',
1284            'claim' => 'lis_result_sourcedid',
1285            'isarray' => false
1286        ];
1287        $endpoint = 'https://www.example.com/moodle';
1288        $oauthconsumerkey = 'consumerkey';
1289        $nonce = '';
1290
1291        $jwt = lti_sign_jwt($params, $endpoint, $oauthconsumerkey, $typeid, $nonce);
1292
1293        $this->assertArrayHasKey('id_token', $jwt);
1294        $this->assertNotEmpty($jwt['id_token']);
1295    }
1296
1297    /**
1298     * Test lti_convert_from_jwt()
1299     */
1300    public function test_lti_convert_from_jwt() {
1301        $this->resetAfterTest();
1302
1303        $this->setAdminUser();
1304
1305        // Create a tool type, associated with that proxy.
1306        $type = new stdClass();
1307        $type->state = LTI_TOOL_STATE_CONFIGURED;
1308        $type->name = "Test tool";
1309        $type->description = "Example description";
1310        $type->clientid = 'sso.example.com';
1311        $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1312
1313        $config = new stdClass();
1314        $config->lti_publickey = '-----BEGIN PUBLIC KEY-----
1315MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv
1316vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc
1317aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy
1318tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0
1319e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb
1320V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9
1321MwIDAQAB
1322-----END PUBLIC KEY-----';
1323        $config->lti_keytype = LTI_RSA_KEY;
1324
1325        $typeid = lti_add_type($type, $config);
1326
1327        $params = lti_convert_from_jwt($typeid, 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwib' .
1328            'mFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiaXNzIjoic3NvLmV4YW1wbGUuY29tIn0.XURVvEb5ueAvFsn-S9EB' .
1329            'BSfKbsgUzfRQqmJ6evlrYdx7sXWoZXw1nYjaLTg-mawvBr7MVvrdG9qh6oN8OfkQ7bfMwiz4tjBMJ4B4q_sig5BDYIKwMNjZL5GGCBs89FQrgqZBhxw' .
1330            '3exTjPBEn69__w40o0AhCsBohPMh0ZsAyHug5dhm8vIuOP667repUJzM8uKCD6L4bEL6vQE8EwU6WQOmfJ2SDmRs-1pFkiaFd6hmPn6AVX7ETtzQmlT' .
1331            'X-nXe9weQjU1lH4AQG2Yfnn-7lS94bt6E76Zt-XndP3IY7W48EpnRfUK9Ff1fZlomT4MPahdNP1eP8gT2iMz7vYpCfmA');
1332
1333        $this->assertEquals('sso.example.com', $params['oauth_consumer_key']);
1334        $this->assertEquals('John Doe', $params['lis_person_name_full']);
1335    }
1336
1337    /**
1338     * Test lti_get_permitted_service_scopes().
1339     */
1340    public function test_lti_get_permitted_service_scopes() {
1341        $this->resetAfterTest();
1342
1343        $this->setAdminUser();
1344
1345        // Create a tool type, associated with that proxy.
1346        $type = new stdClass();
1347        $type->state = LTI_TOOL_STATE_CONFIGURED;
1348        $type->name = "Test tool";
1349        $type->description = "Example description";
1350        $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1351
1352        $typeconfig = new stdClass();
1353        $typeconfig->lti_acceptgrades = true;
1354
1355        $typeid = lti_add_type($type, $typeconfig);
1356
1357        $tool = lti_get_type($typeid);
1358
1359        $config = lti_get_type_config($typeid);
1360        $permittedscopes = lti_get_permitted_service_scopes($tool, $config);
1361
1362        $expected = [
1363            'https://purl.imsglobal.org/spec/lti-bo/scope/basicoutcome'
1364        ];
1365        $this->assertEquals($expected, $permittedscopes);
1366    }
1367
1368    /**
1369     * Test get_tool_type_config().
1370     */
1371    public function test_get_tool_type_config() {
1372        $this->resetAfterTest();
1373
1374        $this->setAdminUser();
1375
1376        // Create a tool type, associated with that proxy.
1377        $type = new stdClass();
1378        $type->state = LTI_TOOL_STATE_CONFIGURED;
1379        $type->name = "Test tool";
1380        $type->description = "Example description";
1381        $type->clientid = "Test client ID";
1382        $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1383
1384        $config = new stdClass();
1385
1386        $typeid = lti_add_type($type, $config);
1387
1388        $type = lti_get_type($typeid);
1389
1390        $typeconfig = get_tool_type_config($type);
1391
1392        $this->assertEquals('https://www.example.com/moodle', $typeconfig['platformid']);
1393        $this->assertEquals($type->clientid, $typeconfig['clientid']);
1394        $this->assertEquals($typeid, $typeconfig['deploymentid']);
1395        $this->assertEquals('https://www.example.com/moodle/mod/lti/certs.php', $typeconfig['publickeyseturl']);
1396        $this->assertEquals('https://www.example.com/moodle/mod/lti/token.php', $typeconfig['accesstokenurl']);
1397        $this->assertEquals('https://www.example.com/moodle/mod/lti/auth.php', $typeconfig['authrequesturl']);
1398    }
1399
1400    /**
1401     * Test lti_new_access_token().
1402     */
1403    public function test_lti_new_access_token() {
1404        global $DB;
1405
1406        $this->resetAfterTest();
1407
1408        $this->setAdminUser();
1409
1410        // Create a tool type, associated with that proxy.
1411        $type = new stdClass();
1412        $type->state = LTI_TOOL_STATE_CONFIGURED;
1413        $type->name = "Test tool";
1414        $type->description = "Example description";
1415        $type->clientid = "Test client ID";
1416        $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1417
1418        $config = new stdClass();
1419
1420        $typeid = lti_add_type($type, $config);
1421
1422        $scopes = ['lti_some_scope', 'lti_another_scope'];
1423
1424        lti_new_access_token($typeid, $scopes);
1425
1426        $token = $DB->get_records('lti_access_tokens');
1427        $this->assertEquals(1, count($token));
1428
1429        $token = reset($token);
1430
1431        $this->assertEquals($typeid, $token->typeid);
1432        $this->assertEquals(json_encode(array_values($scopes)), $token->scope);
1433        $this->assertEquals($token->timecreated + LTI_ACCESS_TOKEN_LIFE, $token->validuntil);
1434        $this->assertNull($token->lastaccess);
1435    }
1436
1437    /**
1438     * Test lti_build_login_request().
1439     */
1440    public function test_lti_build_login_request() {
1441        global $USER, $CFG;
1442
1443        $this->resetAfterTest();
1444
1445        $USER->id = 123456789;
1446
1447        $course   = $this->getDataGenerator()->create_course();
1448        $instance = $this->getDataGenerator()->create_module('lti',
1449            [
1450                'course' => $course->id,
1451            ]
1452        );
1453
1454        $config = new stdClass();
1455        $config->lti_clientid = 'some-client-id';
1456        $config->typeid = 'some-type-id';
1457        $config->lti_toolurl = 'some-lti-tool-url';
1458
1459        $request = lti_build_login_request($course->id, $instance->id, $instance, $config, 'basic-lti-launch-request');
1460
1461        $this->assertEquals($CFG->wwwroot, $request['iss']);
1462        $this->assertEquals('http://some-lti-tool-url', $request['target_link_uri']);
1463        $this->assertEquals(123456789, $request['login_hint']);
1464        $this->assertEquals($instance->id, $request['lti_message_hint']);
1465        $this->assertEquals('some-client-id', $request['client_id']);
1466        $this->assertEquals('some-type-id', $request['lti_deployment_id']);
1467    }
1468
1469    /**
1470     * Test default orgid is host if not specified in config (tool installed in earlier version of Moodle).
1471     */
1472    public function test_lti_get_launch_data_default_organizationid_unset_usehost() {
1473        global $DB;
1474        $this->resetAfterTest();
1475        $this->setAdminUser();
1476        $config = new stdClass();
1477        $config->lti_organizationid = '';
1478        $course = $this->getDataGenerator()->create_course();
1479        $type = $this->create_type($config);
1480        $link = $this->create_instance($type, $course);
1481        $launchdata = lti_get_launch_data($link);
1482        $this->assertEquals($launchdata[1]['tool_consumer_instance_guid'], 'www.example.com');
1483    }
1484
1485    /**
1486     * Test default org id is set to host when config is usehost.
1487     */
1488    public function test_lti_get_launch_data_default_organizationid_set_usehost() {
1489        global $DB;
1490        $this->resetAfterTest();
1491        $this->setAdminUser();
1492        $config = new stdClass();
1493        $config->lti_organizationid = '';
1494        $config->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEHOST;
1495        $course = $this->getDataGenerator()->create_course();
1496        $type = $this->create_type($config);
1497        $link = $this->create_instance($type, $course);
1498        $launchdata = lti_get_launch_data($link);
1499        $this->assertEquals($launchdata[1]['tool_consumer_instance_guid'], 'www.example.com');
1500    }
1501
1502    /**
1503     * Test default org id is set to site id when config is usesiteid.
1504     */
1505    public function test_lti_get_launch_data_default_organizationid_set_usesiteid() {
1506        global $DB;
1507        $this->resetAfterTest();
1508        $this->setAdminUser();
1509        $config = new stdClass();
1510        $config->lti_organizationid = '';
1511        $config->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEID;
1512        $course = $this->getDataGenerator()->create_course();
1513        $type = $this->create_type($config);
1514        $link = $this->create_instance($type, $course);
1515        $launchdata = lti_get_launch_data($link);
1516        $this->assertEquals($launchdata[1]['tool_consumer_instance_guid'], md5(get_site_identifier()));
1517    }
1518
1519    /**
1520     * Test orgid can be overridden in which case default is ignored.
1521     */
1522    public function test_lti_get_launch_data_default_organizationid_orgid_override() {
1523        global $DB;
1524        $this->resetAfterTest();
1525        $this->setAdminUser();
1526        $config = new stdClass();
1527        $config->lti_organizationid = 'overridden!';
1528        $config->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEID;
1529        $course = $this->getDataGenerator()->create_course();
1530        $type = $this->create_type($config);
1531        $link = $this->create_instance($type, $course);
1532        $launchdata = lti_get_launch_data($link);
1533        $this->assertEquals($launchdata[1]['tool_consumer_instance_guid'], 'overridden!');
1534    }
1535
1536    /**
1537     * Create an LTI Tool.
1538     *
1539     * @param object $config tool config.
1540     *
1541     * @return object tool.
1542     */
1543    private function create_type(object $config) {
1544        $type = new stdClass();
1545        $type->state = LTI_TOOL_STATE_CONFIGURED;
1546        $type->name = "Test tool";
1547        $type->description = "Example description";
1548        $type->clientid = "Test client ID";
1549        $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1550
1551        $configbase = new stdClass();
1552        $configbase->lti_acceptgrades = LTI_SETTING_NEVER;
1553        $configbase->lti_sendname = LTI_SETTING_NEVER;
1554        $configbase->lti_sendemailaddr = LTI_SETTING_NEVER;
1555        $mergedconfig = (object) array_merge( (array) $configbase, (array) $config);
1556        $typeid = lti_add_type($type, $mergedconfig);
1557        return lti_get_type($typeid);
1558    }
1559
1560    /**
1561     * Create an LTI Instance for the tool in a given course.
1562     *
1563     * @param object $type tool for which an instance should be added.
1564     * @param object $course course where the instance should be added.
1565     *
1566     * @return object instance.
1567     */
1568    private function create_instance(object $type, object $course) {
1569        $generator = $this->getDataGenerator()->get_plugin_generator('mod_lti');
1570        return $generator->create_instance(array('course' => $course->id,
1571                  'toolurl' => $type->baseurl,
1572                  'typeid' => $type->id
1573                  ), array());
1574    }
1575}
1576