PageRenderTime 41ms CodeModel.GetById 9ms RepoModel.GetById 0ms app.codeStats 1ms

/mod/lti/locallib.php

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