PageRenderTime 56ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/ltiprovider/src/ToolProvider/ToolProvider.php

https://bitbucket.org/moodle/moodle
PHP | 1273 lines | 834 code | 100 blank | 339 comment | 251 complexity | ad6d7f97db4e133b33b21fa6ac5d82b5 MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. namespace IMSGlobal\LTI\ToolProvider;
  3. use IMSGlobal\LTI\Profile\Item;
  4. use IMSGlobal\LTI\ToolProvider\DataConnector\DataConnector;
  5. use IMSGlobal\LTI\ToolProvider\MediaType;
  6. use IMSGlobal\LTI\Profile;
  7. use IMSGlobal\LTI\HTTPMessage;
  8. use IMSGlobal\LTI\OAuth;
  9. /**
  10. * Class to represent an LTI Tool Provider
  11. *
  12. * @author Stephen P Vickers <svickers@imsglobal.org>
  13. * @copyright IMS Global Learning Consortium Inc
  14. * @date 2016
  15. * @version 3.0.2
  16. * @license GNU Lesser General Public License, version 3 (<http://www.gnu.org/licenses/lgpl.html>)
  17. */
  18. class ToolProvider
  19. {
  20. /**
  21. * Default connection error message.
  22. */
  23. const CONNECTION_ERROR_MESSAGE = 'Sorry, there was an error connecting you to the application.';
  24. /**
  25. * LTI version 1 for messages.
  26. */
  27. const LTI_VERSION1 = 'LTI-1p0';
  28. /**
  29. * LTI version 2 for messages.
  30. */
  31. const LTI_VERSION2 = 'LTI-2p0';
  32. /**
  33. * Use ID value only.
  34. */
  35. const ID_SCOPE_ID_ONLY = 0;
  36. /**
  37. * Prefix an ID with the consumer key.
  38. */
  39. const ID_SCOPE_GLOBAL = 1;
  40. /**
  41. * Prefix the ID with the consumer key and context ID.
  42. */
  43. const ID_SCOPE_CONTEXT = 2;
  44. /**
  45. * Prefix the ID with the consumer key and resource ID.
  46. */
  47. const ID_SCOPE_RESOURCE = 3;
  48. /**
  49. * Character used to separate each element of an ID.
  50. */
  51. const ID_SCOPE_SEPARATOR = ':';
  52. /**
  53. * Permitted LTI versions for messages.
  54. */
  55. private static $LTI_VERSIONS = array(self::LTI_VERSION1, self::LTI_VERSION2);
  56. /**
  57. * List of supported message types and associated class methods.
  58. */
  59. private static $MESSAGE_TYPES = array('basic-lti-launch-request' => 'onLaunch',
  60. 'ContentItemSelectionRequest' => 'onContentItem',
  61. 'ToolProxyRegistrationRequest' => 'register');
  62. /**
  63. * List of supported message types and associated class methods
  64. *
  65. * @var array $METHOD_NAMES
  66. */
  67. private static $METHOD_NAMES = array('basic-lti-launch-request' => 'onLaunch',
  68. 'ContentItemSelectionRequest' => 'onContentItem',
  69. 'ToolProxyRegistrationRequest' => 'onRegister');
  70. /**
  71. * Names of LTI parameters to be retained in the consumer settings property.
  72. *
  73. * @var array $LTI_CONSUMER_SETTING_NAMES
  74. */
  75. private static $LTI_CONSUMER_SETTING_NAMES = array('custom_tc_profile_url', 'custom_system_setting_url');
  76. /**
  77. * Names of LTI parameters to be retained in the context settings property.
  78. *
  79. * @var array $LTI_CONTEXT_SETTING_NAMES
  80. */
  81. private static $LTI_CONTEXT_SETTING_NAMES = array('custom_context_setting_url',
  82. 'custom_lineitems_url', 'custom_results_url',
  83. 'custom_context_memberships_url');
  84. /**
  85. * Names of LTI parameters to be retained in the resource link settings property.
  86. *
  87. * @var array $LTI_RESOURCE_LINK_SETTING_NAMES
  88. */
  89. private static $LTI_RESOURCE_LINK_SETTING_NAMES = array('lis_course_section_sourcedid', 'lis_result_sourcedid', 'lis_outcome_service_url',
  90. 'ext_ims_lis_basic_outcome_url', 'ext_ims_lis_resultvalue_sourcedids',
  91. 'ext_ims_lis_memberships_id', 'ext_ims_lis_memberships_url',
  92. 'ext_ims_lti_tool_setting', 'ext_ims_lti_tool_setting_id', 'ext_ims_lti_tool_setting_url',
  93. 'custom_link_setting_url',
  94. 'custom_lineitem_url', 'custom_result_url');
  95. /**
  96. * Names of LTI custom parameter substitution variables (or capabilities) and their associated default message parameter names.
  97. *
  98. * @var array $CUSTOM_SUBSTITUTION_VARIABLES
  99. */
  100. private static $CUSTOM_SUBSTITUTION_VARIABLES = array('User.id' => 'user_id',
  101. 'User.image' => 'user_image',
  102. 'User.username' => 'username',
  103. 'User.scope.mentor' => 'role_scope_mentor',
  104. 'Membership.role' => 'roles',
  105. 'Person.sourcedId' => 'lis_person_sourcedid',
  106. 'Person.name.full' => 'lis_person_name_full',
  107. 'Person.name.family' => 'lis_person_name_family',
  108. 'Person.name.given' => 'lis_person_name_given',
  109. 'Person.email.primary' => 'lis_person_contact_email_primary',
  110. 'Context.id' => 'context_id',
  111. 'Context.type' => 'context_type',
  112. 'Context.title' => 'context_title',
  113. 'Context.label' => 'context_label',
  114. 'CourseOffering.sourcedId' => 'lis_course_offering_sourcedid',
  115. 'CourseSection.sourcedId' => 'lis_course_section_sourcedid',
  116. 'CourseSection.label' => 'context_label',
  117. 'CourseSection.title' => 'context_title',
  118. 'ResourceLink.id' => 'resource_link_id',
  119. 'ResourceLink.title' => 'resource_link_title',
  120. 'ResourceLink.description' => 'resource_link_description',
  121. 'Result.sourcedId' => 'lis_result_sourcedid',
  122. 'BasicOutcome.url' => 'lis_outcome_service_url',
  123. 'ToolConsumerProfile.url' => 'custom_tc_profile_url',
  124. 'ToolProxy.url' => 'tool_proxy_url',
  125. 'ToolProxy.custom.url' => 'custom_system_setting_url',
  126. 'ToolProxyBinding.custom.url' => 'custom_context_setting_url',
  127. 'LtiLink.custom.url' => 'custom_link_setting_url',
  128. 'LineItems.url' => 'custom_lineitems_url',
  129. 'LineItem.url' => 'custom_lineitem_url',
  130. 'Results.url' => 'custom_results_url',
  131. 'Result.url' => 'custom_result_url',
  132. 'ToolProxyBinding.memberships.url' => 'custom_context_memberships_url');
  133. /**
  134. * True if the last request was successful.
  135. *
  136. * @var boolean $ok
  137. */
  138. public $ok = true;
  139. /**
  140. * Tool Consumer object.
  141. *
  142. * @var ToolConsumer $consumer
  143. */
  144. public $consumer = null;
  145. /**
  146. * Return URL provided by tool consumer.
  147. *
  148. * @var string $returnUrl
  149. */
  150. public $returnUrl = null;
  151. /**
  152. * User object.
  153. *
  154. * @var User $user
  155. */
  156. public $user = null;
  157. /**
  158. * Resource link object.
  159. *
  160. * @var ResourceLink $resourceLink
  161. */
  162. public $resourceLink = null;
  163. /**
  164. * Context object.
  165. *
  166. * @var Context $context
  167. */
  168. public $context = null;
  169. /**
  170. * Data connector object.
  171. *
  172. * @var DataConnector $dataConnector
  173. */
  174. public $dataConnector = null;
  175. /**
  176. * Default email domain.
  177. *
  178. * @var string $defaultEmail
  179. */
  180. public $defaultEmail = '';
  181. /**
  182. * Scope to use for user IDs.
  183. *
  184. * @var int $idScope
  185. */
  186. public $idScope = self::ID_SCOPE_ID_ONLY;
  187. /**
  188. * Whether shared resource link arrangements are permitted.
  189. *
  190. * @var boolean $allowSharing
  191. */
  192. public $allowSharing = false;
  193. /**
  194. * Message for last request processed
  195. *
  196. * @var string $message
  197. */
  198. public $message = self::CONNECTION_ERROR_MESSAGE;
  199. /**
  200. * Error message for last request processed.
  201. *
  202. * @var string $reason
  203. */
  204. public $reason = null;
  205. /**
  206. * Details for error message relating to last request processed.
  207. *
  208. * @var array $details
  209. */
  210. public $details = array();
  211. /**
  212. * Base URL for tool provider service
  213. *
  214. * @var string $baseUrl
  215. */
  216. public $baseUrl = null;
  217. /**
  218. * Vendor details
  219. *
  220. * @var Item $vendor
  221. */
  222. public $vendor = null;
  223. /**
  224. * Product details
  225. *
  226. * @var Item $product
  227. */
  228. public $product = null;
  229. /**
  230. * Services required by Tool Provider
  231. *
  232. * @var array $requiredServices
  233. */
  234. public $requiredServices = null;
  235. /**
  236. * Optional services used by Tool Provider
  237. *
  238. * @var array $optionalServices
  239. */
  240. public $optionalServices = null;
  241. /**
  242. * Resource handlers for Tool Provider
  243. *
  244. * @var array $resourceHandlers
  245. */
  246. public $resourceHandlers = null;
  247. /**
  248. * URL to redirect user to on successful completion of the request.
  249. *
  250. * @var string $redirectUrl
  251. */
  252. protected $redirectUrl = null;
  253. /**
  254. * URL to redirect user to on successful completion of the request.
  255. *
  256. * @var string $mediaTypes
  257. */
  258. protected $mediaTypes = null;
  259. /**
  260. * URL to redirect user to on successful completion of the request.
  261. *
  262. * @var string $documentTargets
  263. */
  264. protected $documentTargets = null;
  265. /**
  266. * HTML to be displayed on a successful completion of the request.
  267. *
  268. * @var string $output
  269. */
  270. protected $output = null;
  271. /**
  272. * HTML to be displayed on an unsuccessful completion of the request and no return URL is available.
  273. *
  274. * @var string $errorOutput
  275. */
  276. protected $errorOutput = null;
  277. /**
  278. * Whether debug messages explaining the cause of errors are to be returned to the tool consumer.
  279. *
  280. * @var boolean $debugMode
  281. */
  282. protected $debugMode = false;
  283. /**
  284. * Callback functions for handling requests.
  285. *
  286. * @var array $callbackHandler
  287. */
  288. private $callbackHandler = null;
  289. /**
  290. * LTI parameter constraints for auto validation checks.
  291. *
  292. * @var array $constraints
  293. */
  294. private $constraints = null;
  295. /**
  296. * Class constructor
  297. *
  298. * @param DataConnector $dataConnector Object containing a database connection object
  299. */
  300. function __construct($dataConnector)
  301. {
  302. $this->constraints = array();
  303. $this->dataConnector = $dataConnector;
  304. $this->ok = !is_null($this->dataConnector);
  305. // Set debug mode
  306. $this->debugMode = isset($_POST['custom_debug']) && (strtolower($_POST['custom_debug']) === 'true');
  307. // Set return URL if available
  308. if (isset($_POST['launch_presentation_return_url'])) {
  309. $this->returnUrl = $_POST['launch_presentation_return_url'];
  310. } else if (isset($_POST['content_item_return_url'])) {
  311. $this->returnUrl = $_POST['content_item_return_url'];
  312. }
  313. $this->vendor = new Profile\Item();
  314. $this->product = new Profile\Item();
  315. $this->requiredServices = array();
  316. $this->optionalServices = array();
  317. $this->resourceHandlers = array();
  318. }
  319. /**
  320. * Process an incoming request
  321. */
  322. public function handleRequest()
  323. {
  324. if ($this->ok) {
  325. if ($this->authenticate()) {
  326. $this->doCallback();
  327. }
  328. }
  329. $this->result();
  330. }
  331. /**
  332. * Add a parameter constraint to be checked on launch
  333. *
  334. * @param string $name Name of parameter to be checked
  335. * @param boolean $required True if parameter is required (optional, default is true)
  336. * @param int $maxLength Maximum permitted length of parameter value (optional, default is null)
  337. * @param array $messageTypes Array of message types to which the constraint applies (optional, default is all)
  338. */
  339. public function setParameterConstraint($name, $required = true, $maxLength = null, $messageTypes = null)
  340. {
  341. $name = trim($name);
  342. if (strlen($name) > 0) {
  343. $this->constraints[$name] = array('required' => $required, 'max_length' => $maxLength, 'messages' => $messageTypes);
  344. }
  345. }
  346. /**
  347. * Get an array of defined tool consumers
  348. *
  349. * @return array Array of ToolConsumer objects
  350. */
  351. public function getConsumers()
  352. {
  353. return $this->dataConnector->getToolConsumers();
  354. }
  355. /**
  356. * Find an offered service based on a media type and HTTP action(s)
  357. *
  358. * @param string $format Media type required
  359. * @param array $methods Array of HTTP actions required
  360. *
  361. * @return object The service object
  362. */
  363. public function findService($format, $methods)
  364. {
  365. $found = false;
  366. $services = $this->consumer->profile->service_offered;
  367. if (is_array($services)) {
  368. $n = -1;
  369. foreach ($services as $service) {
  370. $n++;
  371. if (!is_array($service->format) || !in_array($format, $service->format)) {
  372. continue;
  373. }
  374. $missing = array();
  375. foreach ($methods as $method) {
  376. if (!is_array($service->action) || !in_array($method, $service->action)) {
  377. $missing[] = $method;
  378. }
  379. }
  380. $methods = $missing;
  381. if (count($methods) <= 0) {
  382. $found = $service;
  383. break;
  384. }
  385. }
  386. }
  387. return $found;
  388. }
  389. /**
  390. * Send the tool proxy to the Tool Consumer
  391. *
  392. * @return boolean True if the tool proxy was accepted
  393. */
  394. public function doToolProxyService()
  395. {
  396. // Create tool proxy
  397. $toolProxyService = $this->findService('application/vnd.ims.lti.v2.toolproxy+json', array('POST'));
  398. $secret = DataConnector::getRandomString(12);
  399. $toolProxy = new MediaType\ToolProxy($this, $toolProxyService, $secret);
  400. $http = $this->consumer->doServiceRequest($toolProxyService, 'POST', 'application/vnd.ims.lti.v2.toolproxy+json', json_encode($toolProxy));
  401. $ok = $http->ok && ($http->status == 201) && isset($http->responseJson->tool_proxy_guid) && (strlen($http->responseJson->tool_proxy_guid) > 0);
  402. if ($ok) {
  403. $this->consumer->setKey($http->responseJson->tool_proxy_guid);
  404. $this->consumer->secret = $toolProxy->security_contract->shared_secret;
  405. $this->consumer->toolProxy = json_encode($toolProxy);
  406. $this->consumer->save();
  407. }
  408. return $ok;
  409. }
  410. /**
  411. * Get an array of fully qualified user roles
  412. *
  413. * @param mixed $roles Comma-separated list of roles or array of roles
  414. *
  415. * @return array Array of roles
  416. */
  417. public static function parseRoles($roles)
  418. {
  419. if (!is_array($roles)) {
  420. $roles = explode(',', $roles);
  421. }
  422. $parsedRoles = array();
  423. foreach ($roles as $role) {
  424. $role = trim($role);
  425. if (!empty($role)) {
  426. if (substr($role, 0, 4) !== 'urn:') {
  427. $role = 'urn:lti:role:ims/lis/' . $role;
  428. }
  429. $parsedRoles[] = $role;
  430. }
  431. }
  432. return $parsedRoles;
  433. }
  434. /**
  435. * Generate a web page containing an auto-submitted form of parameters.
  436. *
  437. * @param string $url URL to which the form should be submitted
  438. * @param array $params Array of form parameters
  439. * @param string $target Name of target (optional)
  440. * @return string
  441. */
  442. public static function sendForm($url, $params, $target = '')
  443. {
  444. $page = <<< EOD
  445. <html>
  446. <head>
  447. <title>IMS LTI message</title>
  448. <script type="text/javascript">
  449. //<![CDATA[
  450. function doOnLoad() {
  451. document.forms[0].submit();
  452. }
  453. window.onload=doOnLoad;
  454. //]]>
  455. </script>
  456. </head>
  457. <body>
  458. <form action="{$url}" method="post" target="" encType="application/x-www-form-urlencoded">
  459. EOD;
  460. foreach($params as $key => $value ) {
  461. $key = htmlentities($key, ENT_COMPAT | ENT_HTML401, 'UTF-8');
  462. $value = htmlentities($value, ENT_COMPAT | ENT_HTML401, 'UTF-8');
  463. $page .= <<< EOD
  464. <input type="hidden" name="{$key}" value="{$value}" />
  465. EOD;
  466. }
  467. $page .= <<< EOD
  468. </form>
  469. </body>
  470. </html>
  471. EOD;
  472. return $page;
  473. }
  474. ###
  475. ### PROTECTED METHODS
  476. ###
  477. /**
  478. * Process a valid launch request
  479. *
  480. * @return boolean True if no error
  481. */
  482. protected function onLaunch()
  483. {
  484. $this->onError();
  485. }
  486. /**
  487. * Process a valid content-item request
  488. *
  489. * @return boolean True if no error
  490. */
  491. protected function onContentItem()
  492. {
  493. $this->onError();
  494. }
  495. /**
  496. * Process a valid tool proxy registration request
  497. *
  498. * @return boolean True if no error
  499. */
  500. protected function onRegister() {
  501. $this->onError();
  502. }
  503. /**
  504. * Process a response to an invalid request
  505. *
  506. * @return boolean True if no further error processing required
  507. */
  508. protected function onError()
  509. {
  510. $this->doCallback('onError');
  511. }
  512. ###
  513. ### PRIVATE METHODS
  514. ###
  515. /**
  516. * Call any callback function for the requested action.
  517. *
  518. * This function may set the redirect_url and output properties.
  519. *
  520. * @return boolean True if no error reported
  521. */
  522. private function doCallback($method = null)
  523. {
  524. $callback = $method;
  525. if (is_null($callback)) {
  526. $callback = self::$METHOD_NAMES[$_POST['lti_message_type']];
  527. }
  528. if (method_exists($this, $callback)) {
  529. $result = $this->$callback();
  530. } else if (is_null($method) && $this->ok) {
  531. $this->ok = false;
  532. $this->reason = "Message type not supported: {$_POST['lti_message_type']}";
  533. }
  534. if ($this->ok && ($_POST['lti_message_type'] == 'ToolProxyRegistrationRequest')) {
  535. $this->consumer->save();
  536. }
  537. }
  538. /**
  539. * Perform the result of an action.
  540. *
  541. * This function may redirect the user to another URL rather than returning a value.
  542. *
  543. * @return string Output to be displayed (redirection, or display HTML or message)
  544. */
  545. private function result()
  546. {
  547. $ok = false;
  548. if (!$this->ok) {
  549. $ok = $this->onError();
  550. }
  551. if (!$ok) {
  552. if (!$this->ok) {
  553. // If not valid, return an error message to the tool consumer if a return URL is provided
  554. if (!empty($this->returnUrl)) {
  555. $errorUrl = $this->returnUrl;
  556. if (strpos($errorUrl, '?') === false) {
  557. $errorUrl .= '?';
  558. } else {
  559. $errorUrl .= '&';
  560. }
  561. if ($this->debugMode && !is_null($this->reason)) {
  562. $errorUrl .= 'lti_errormsg=' . urlencode("Debug error: $this->reason");
  563. } else {
  564. $errorUrl .= 'lti_errormsg=' . urlencode($this->message);
  565. if (!is_null($this->reason)) {
  566. $errorUrl .= '&lti_errorlog=' . urlencode("Debug error: $this->reason");
  567. }
  568. }
  569. if (!is_null($this->consumer) && isset($_POST['lti_message_type']) && ($_POST['lti_message_type'] === 'ContentItemSelectionRequest')) {
  570. $formParams = array();
  571. if (isset($_POST['data'])) {
  572. $formParams['data'] = $_POST['data'];
  573. }
  574. $version = (isset($_POST['lti_version'])) ? $_POST['lti_version'] : self::LTI_VERSION1;
  575. $formParams = $this->consumer->signParameters($errorUrl, 'ContentItemSelection', $version, $formParams);
  576. $page = self::sendForm($errorUrl, $formParams);
  577. echo $page;
  578. } else {
  579. header("Location: {$errorUrl}");
  580. }
  581. exit;
  582. } else {
  583. if (!is_null($this->errorOutput)) {
  584. echo $this->errorOutput;
  585. } else if ($this->debugMode && !empty($this->reason)) {
  586. echo "Debug error: {$this->reason}";
  587. } else {
  588. echo "Error: {$this->message}";
  589. }
  590. }
  591. } else if (!is_null($this->redirectUrl)) {
  592. header("Location: {$this->redirectUrl}");
  593. exit;
  594. } else if (!is_null($this->output)) {
  595. echo $this->output;
  596. }
  597. }
  598. }
  599. /**
  600. * Check the authenticity of the LTI launch request.
  601. *
  602. * The consumer, resource link and user objects will be initialised if the request is valid.
  603. *
  604. * @return boolean True if the request has been successfully validated.
  605. */
  606. private function authenticate()
  607. {
  608. // Get the consumer
  609. $doSaveConsumer = false;
  610. // Check all required launch parameters
  611. $this->ok = isset($_POST['lti_message_type']) && array_key_exists($_POST['lti_message_type'], self::$MESSAGE_TYPES);
  612. if (!$this->ok) {
  613. $this->reason = 'Invalid or missing lti_message_type parameter.';
  614. }
  615. if ($this->ok) {
  616. $this->ok = isset($_POST['lti_version']) && in_array($_POST['lti_version'], self::$LTI_VERSIONS);
  617. if (!$this->ok) {
  618. $this->reason = 'Invalid or missing lti_version parameter.';
  619. }
  620. }
  621. if ($this->ok) {
  622. if ($_POST['lti_message_type'] === 'basic-lti-launch-request') {
  623. $this->ok = isset($_POST['resource_link_id']) && (strlen(trim($_POST['resource_link_id'])) > 0);
  624. if (!$this->ok) {
  625. $this->reason = 'Missing resource link ID.';
  626. }
  627. } else if ($_POST['lti_message_type'] === 'ContentItemSelectionRequest') {
  628. if (isset($_POST['accept_media_types']) && (strlen(trim($_POST['accept_media_types'])) > 0)) {
  629. $mediaTypes = array_filter(explode(',', str_replace(' ', '', $_POST['accept_media_types'])), 'strlen');
  630. $mediaTypes = array_unique($mediaTypes);
  631. $this->ok = count($mediaTypes) > 0;
  632. if (!$this->ok) {
  633. $this->reason = 'No accept_media_types found.';
  634. } else {
  635. $this->mediaTypes = $mediaTypes;
  636. }
  637. } else {
  638. $this->ok = false;
  639. }
  640. if ($this->ok && isset($_POST['accept_presentation_document_targets']) && (strlen(trim($_POST['accept_presentation_document_targets'])) > 0)) {
  641. $documentTargets = array_filter(explode(',', str_replace(' ', '', $_POST['accept_presentation_document_targets'])), 'strlen');
  642. $documentTargets = array_unique($documentTargets);
  643. $this->ok = count($documentTargets) > 0;
  644. if (!$this->ok) {
  645. $this->reason = 'Missing or empty accept_presentation_document_targets parameter.';
  646. } else {
  647. foreach ($documentTargets as $documentTarget) {
  648. $this->ok = $this->checkValue($documentTarget, array('embed', 'frame', 'iframe', 'window', 'popup', 'overlay', 'none'),
  649. 'Invalid value in accept_presentation_document_targets parameter: %s.');
  650. if (!$this->ok) {
  651. break;
  652. }
  653. }
  654. if ($this->ok) {
  655. $this->documentTargets = $documentTargets;
  656. }
  657. }
  658. } else {
  659. $this->ok = false;
  660. }
  661. if ($this->ok) {
  662. $this->ok = isset($_POST['content_item_return_url']) && (strlen(trim($_POST['content_item_return_url'])) > 0);
  663. if (!$this->ok) {
  664. $this->reason = 'Missing content_item_return_url parameter.';
  665. }
  666. }
  667. } else if ($_POST['lti_message_type'] == 'ToolProxyRegistrationRequest') {
  668. $this->ok = ((isset($_POST['reg_key']) && (strlen(trim($_POST['reg_key'])) > 0)) &&
  669. (isset($_POST['reg_password']) && (strlen(trim($_POST['reg_password'])) > 0)) &&
  670. (isset($_POST['tc_profile_url']) && (strlen(trim($_POST['tc_profile_url'])) > 0)) &&
  671. (isset($_POST['launch_presentation_return_url']) && (strlen(trim($_POST['launch_presentation_return_url'])) > 0)));
  672. if ($this->debugMode && !$this->ok) {
  673. $this->reason = 'Missing message parameters.';
  674. }
  675. }
  676. }
  677. $now = time();
  678. // Check consumer key
  679. if ($this->ok && ($_POST['lti_message_type'] != 'ToolProxyRegistrationRequest')) {
  680. $this->ok = isset($_POST['oauth_consumer_key']);
  681. if (!$this->ok) {
  682. $this->reason = 'Missing consumer key.';
  683. }
  684. if ($this->ok) {
  685. $this->consumer = new ToolConsumer($_POST['oauth_consumer_key'], $this->dataConnector);
  686. $this->ok = !is_null($this->consumer->created);
  687. if (!$this->ok) {
  688. $this->reason = 'Invalid consumer key.';
  689. }
  690. }
  691. if ($this->ok) {
  692. $today = date('Y-m-d', $now);
  693. if (is_null($this->consumer->lastAccess)) {
  694. $doSaveConsumer = true;
  695. } else {
  696. $last = date('Y-m-d', $this->consumer->lastAccess);
  697. $doSaveConsumer = $doSaveConsumer || ($last !== $today);
  698. }
  699. $this->consumer->last_access = $now;
  700. try {
  701. $store = new OAuthDataStore($this);
  702. $server = new OAuth\OAuthServer($store);
  703. $method = new OAuth\OAuthSignatureMethod_HMAC_SHA1();
  704. $server->add_signature_method($method);
  705. $request = OAuth\OAuthRequest::from_request();
  706. $res = $server->verify_request($request);
  707. } catch (\Exception $e) {
  708. $this->ok = false;
  709. if (empty($this->reason)) {
  710. if ($this->debugMode) {
  711. $consumer = new OAuth\OAuthConsumer($this->consumer->getKey(), $this->consumer->secret);
  712. $signature = $request->build_signature($method, $consumer, false);
  713. $this->reason = $e->getMessage();
  714. if (empty($this->reason)) {
  715. $this->reason = 'OAuth exception';
  716. }
  717. $this->details[] = 'Timestamp: ' . time();
  718. $this->details[] = "Signature: {$signature}";
  719. $this->details[] = "Base string: {$request->base_string}]";
  720. } else {
  721. $this->reason = 'OAuth signature check failed - perhaps an incorrect secret or timestamp.';
  722. }
  723. }
  724. }
  725. }
  726. if ($this->ok) {
  727. $today = date('Y-m-d', $now);
  728. if (is_null($this->consumer->lastAccess)) {
  729. $doSaveConsumer = true;
  730. } else {
  731. $last = date('Y-m-d', $this->consumer->lastAccess);
  732. $doSaveConsumer = $doSaveConsumer || ($last !== $today);
  733. }
  734. $this->consumer->last_access = $now;
  735. if ($this->consumer->protected) {
  736. if (!is_null($this->consumer->consumerGuid)) {
  737. $this->ok = empty($_POST['tool_consumer_instance_guid']) ||
  738. ($this->consumer->consumerGuid === $_POST['tool_consumer_instance_guid']);
  739. if (!$this->ok) {
  740. $this->reason = 'Request is from an invalid tool consumer.';
  741. }
  742. }
  743. }
  744. if ($this->ok) {
  745. $this->ok = $this->consumer->enabled;
  746. if (!$this->ok) {
  747. $this->reason = 'Tool consumer has not been enabled by the tool provider.';
  748. }
  749. }
  750. if ($this->ok) {
  751. $this->ok = is_null($this->consumer->enableFrom) || ($this->consumer->enableFrom <= $now);
  752. if ($this->ok) {
  753. $this->ok = is_null($this->consumer->enableUntil) || ($this->consumer->enableUntil > $now);
  754. if (!$this->ok) {
  755. $this->reason = 'Tool consumer access has expired.';
  756. }
  757. } else {
  758. $this->reason = 'Tool consumer access is not yet available.';
  759. }
  760. }
  761. }
  762. // Validate other message parameter values
  763. if ($this->ok) {
  764. if ($_POST['lti_message_type'] === 'ContentItemSelectionRequest') {
  765. if (isset($_POST['accept_unsigned'])) {
  766. $this->ok = $this->checkValue($_POST['accept_unsigned'], array('true', 'false'), 'Invalid value for accept_unsigned parameter: %s.');
  767. }
  768. if ($this->ok && isset($_POST['accept_multiple'])) {
  769. $this->ok = $this->checkValue($_POST['accept_multiple'], array('true', 'false'), 'Invalid value for accept_multiple parameter: %s.');
  770. }
  771. if ($this->ok && isset($_POST['accept_copy_advice'])) {
  772. $this->ok = $this->checkValue($_POST['accept_copy_advice'], array('true', 'false'), 'Invalid value for accept_copy_advice parameter: %s.');
  773. }
  774. if ($this->ok && isset($_POST['auto_create'])) {
  775. $this->ok = $this->checkValue($_POST['auto_create'], array('true', 'false'), 'Invalid value for auto_create parameter: %s.');
  776. }
  777. if ($this->ok && isset($_POST['can_confirm'])) {
  778. $this->ok = $this->checkValue($_POST['can_confirm'], array('true', 'false'), 'Invalid value for can_confirm parameter: %s.');
  779. }
  780. } else if (isset($_POST['launch_presentation_document_target'])) {
  781. $this->ok = $this->checkValue($_POST['launch_presentation_document_target'], array('embed', 'frame', 'iframe', 'window', 'popup', 'overlay'),
  782. 'Invalid value for launch_presentation_document_target parameter: %s.');
  783. }
  784. }
  785. }
  786. if ($this->ok && ($_POST['lti_message_type'] === 'ToolProxyRegistrationRequest')) {
  787. $this->ok = $_POST['lti_version'] == self::LTI_VERSION2;
  788. if (!$this->ok) {
  789. $this->reason = 'Invalid lti_version parameter';
  790. }
  791. if ($this->ok) {
  792. $http = new HTTPMessage($_POST['tc_profile_url'], 'GET', null, 'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json');
  793. $this->ok = $http->send();
  794. if (!$this->ok) {
  795. $this->reason = 'Tool consumer profile not accessible.';
  796. } else {
  797. $tcProfile = json_decode($http->response);
  798. $this->ok = !is_null($tcProfile);
  799. if (!$this->ok) {
  800. $this->reason = 'Invalid JSON in tool consumer profile.';
  801. }
  802. }
  803. }
  804. // Check for required capabilities
  805. if ($this->ok) {
  806. $this->consumer = new ToolConsumer($_POST['reg_key'], $this->dataConnector);
  807. $this->consumer->profile = $tcProfile;
  808. $capabilities = $this->consumer->profile->capability_offered;
  809. $missing = array();
  810. foreach ($this->resourceHandlers as $resourceHandler) {
  811. foreach ($resourceHandler->requiredMessages as $message) {
  812. if (!in_array($message->type, $capabilities)) {
  813. $missing[$message->type] = true;
  814. }
  815. }
  816. }
  817. foreach ($this->constraints as $name => $constraint) {
  818. if ($constraint['required']) {
  819. if (!in_array($name, $capabilities) && !in_array($name, array_flip($capabilities))) {
  820. $missing[$name] = true;
  821. }
  822. }
  823. }
  824. if (!empty($missing)) {
  825. ksort($missing);
  826. $this->reason = 'Required capability not offered - \'' . implode('\', \'', array_keys($missing)) . '\'';
  827. $this->ok = false;
  828. }
  829. }
  830. // Check for required services
  831. if ($this->ok) {
  832. foreach ($this->requiredServices as $service) {
  833. foreach ($service->formats as $format) {
  834. if (!$this->findService($format, $service->actions)) {
  835. if ($this->ok) {
  836. $this->reason = 'Required service(s) not offered - ';
  837. $this->ok = false;
  838. } else {
  839. $this->reason .= ', ';
  840. }
  841. $this->reason .= "'{$format}' [" . implode(', ', $service->actions) . ']';
  842. }
  843. }
  844. }
  845. }
  846. if ($this->ok) {
  847. if ($_POST['lti_message_type'] === 'ToolProxyRegistrationRequest') {
  848. $this->consumer->profile = $tcProfile;
  849. $this->consumer->secret = $_POST['reg_password'];
  850. $this->consumer->ltiVersion = $_POST['lti_version'];
  851. $this->consumer->name = $tcProfile->product_instance->service_owner->service_owner_name->default_value;
  852. $this->consumer->consumerName = $this->consumer->name;
  853. $this->consumer->consumerVersion = "{$tcProfile->product_instance->product_info->product_family->code}-{$tcProfile->product_instance->product_info->product_version}";
  854. $this->consumer->consumerGuid = $tcProfile->product_instance->guid;
  855. $this->consumer->enabled = true;
  856. $this->consumer->protected = true;
  857. $doSaveConsumer = true;
  858. }
  859. }
  860. } else if ($this->ok && !empty($_POST['custom_tc_profile_url']) && empty($this->consumer->profile)) {
  861. $http = new HTTPMessage($_POST['custom_tc_profile_url'], 'GET', null, 'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json');
  862. if ($http->send()) {
  863. $tcProfile = json_decode($http->response);
  864. if (!is_null($tcProfile)) {
  865. $this->consumer->profile = $tcProfile;
  866. $doSaveConsumer = true;
  867. }
  868. }
  869. }
  870. // Validate message parameter constraints
  871. if ($this->ok) {
  872. $invalidParameters = array();
  873. foreach ($this->constraints as $name => $constraint) {
  874. if (empty($constraint['messages']) || in_array($_POST['lti_message_type'], $constraint['messages'])) {
  875. $ok = true;
  876. if ($constraint['required']) {
  877. if (!isset($_POST[$name]) || (strlen(trim($_POST[$name])) <= 0)) {
  878. $invalidParameters[] = "{$name} (missing)";
  879. $ok = false;
  880. }
  881. }
  882. if ($ok && !is_null($constraint['max_length']) && isset($_POST[$name])) {
  883. if (strlen(trim($_POST[$name])) > $constraint['max_length']) {
  884. $invalidParameters[] = "{$name} (too long)";
  885. }
  886. }
  887. }
  888. }
  889. if (count($invalidParameters) > 0) {
  890. $this->ok = false;
  891. if (empty($this->reason)) {
  892. $this->reason = 'Invalid parameter(s): ' . implode(', ', $invalidParameters) . '.';
  893. }
  894. }
  895. }
  896. if ($this->ok) {
  897. // Set the request context
  898. if (isset($_POST['context_id'])) {
  899. $this->context = Context::fromConsumer($this->consumer, trim($_POST['context_id']));
  900. $title = '';
  901. if (isset($_POST['context_title'])) {
  902. $title = trim($_POST['context_title']);
  903. }
  904. if (empty($title)) {
  905. $title = "Course {$this->context->getId()}";
  906. }
  907. if (isset($_POST['context_type'])) {
  908. $this->context->type = trim($_POST['context_type']);
  909. }
  910. $this->context->title = $title;
  911. }
  912. // Set the request resource link
  913. if (isset($_POST['resource_link_id'])) {
  914. $contentItemId = '';
  915. if (isset($_POST['custom_content_item_id'])) {
  916. $contentItemId = $_POST['custom_content_item_id'];
  917. }
  918. $this->resourceLink = ResourceLink::fromConsumer($this->consumer, trim($_POST['resource_link_id']), $contentItemId);
  919. if (!empty($this->context)) {
  920. $this->resourceLink->setContextId($this->context->getRecordId());
  921. }
  922. $title = '';
  923. if (isset($_POST['resource_link_title'])) {
  924. $title = trim($_POST['resource_link_title']);
  925. }
  926. if (empty($title)) {
  927. $title = "Resource {$this->resourceLink->getId()}";
  928. }
  929. $this->resourceLink->title = $title;
  930. // Delete any existing custom parameters
  931. foreach ($this->consumer->getSettings() as $name => $value) {
  932. if (strpos($name, 'custom_') === 0) {
  933. $this->consumer->setSetting($name);
  934. $doSaveConsumer = true;
  935. }
  936. }
  937. if (!empty($this->context)) {
  938. foreach ($this->context->getSettings() as $name => $value) {
  939. if (strpos($name, 'custom_') === 0) {
  940. $this->context->setSetting($name);
  941. }
  942. }
  943. }
  944. foreach ($this->resourceLink->getSettings() as $name => $value) {
  945. if (strpos($name, 'custom_') === 0) {
  946. $this->resourceLink->setSetting($name);
  947. }
  948. }
  949. // Save LTI parameters
  950. foreach (self::$LTI_CONSUMER_SETTING_NAMES as $name) {
  951. if (isset($_POST[$name])) {
  952. $this->consumer->setSetting($name, $_POST[$name]);
  953. } else {
  954. $this->consumer->setSetting($name);
  955. }
  956. }
  957. if (!empty($this->context)) {
  958. foreach (self::$LTI_CONTEXT_SETTING_NAMES as $name) {
  959. if (isset($_POST[$name])) {
  960. $this->context->setSetting($name, $_POST[$name]);
  961. } else {
  962. $this->context->setSetting($name);
  963. }
  964. }
  965. }
  966. foreach (self::$LTI_RESOURCE_LINK_SETTING_NAMES as $name) {
  967. if (isset($_POST[$name])) {
  968. $this->resourceLink->setSetting($name, $_POST[$name]);
  969. } else {
  970. $this->resourceLink->setSetting($name);
  971. }
  972. }
  973. // Save other custom parameters
  974. foreach ($_POST as $name => $value) {
  975. if ((strpos($name, 'custom_') === 0) &&
  976. !in_array($name, array_merge(self::$LTI_CONSUMER_SETTING_NAMES, self::$LTI_CONTEXT_SETTING_NAMES, self::$LTI_RESOURCE_LINK_SETTING_NAMES))) {
  977. $this->resourceLink->setSetting($name, $value);
  978. }
  979. }
  980. }
  981. // Set the user instance
  982. $userId = '';
  983. if (isset($_POST['user_id'])) {
  984. $userId = trim($_POST['user_id']);
  985. }
  986. $this->user = User::fromResourceLink($this->resourceLink, $userId);
  987. // Set the user name
  988. $firstname = (isset($_POST['lis_person_name_given'])) ? $_POST['lis_person_name_given'] : '';
  989. $lastname = (isset($_POST['lis_person_name_family'])) ? $_POST['lis_person_name_family'] : '';
  990. $fullname = (isset($_POST['lis_person_name_full'])) ? $_POST['lis_person_name_full'] : '';
  991. $this->user->setNames($firstname, $lastname, $fullname);
  992. // Set the user email
  993. $email = (isset($_POST['lis_person_contact_email_primary'])) ? $_POST['lis_person_contact_email_primary'] : '';
  994. $this->user->setEmail($email, $this->defaultEmail);
  995. // Set the user image URI
  996. if (isset($_POST['user_image'])) {
  997. $this->user->image = $_POST['user_image'];
  998. }
  999. // Set the user roles
  1000. if (isset($_POST['roles'])) {
  1001. $this->user->roles = self::parseRoles($_POST['roles']);
  1002. }
  1003. // Initialise the consumer and check for changes
  1004. $this->consumer->defaultEmail = $this->defaultEmail;
  1005. if ($this->consumer->ltiVersion !== $_POST['lti_version']) {
  1006. $this->consumer->ltiVersion = $_POST['lti_version'];
  1007. $doSaveConsumer = true;
  1008. }
  1009. if (isset($_POST['tool_consumer_instance_name'])) {
  1010. if ($this->consumer->consumerName !== $_POST['tool_consumer_instance_name']) {
  1011. $this->consumer->consumerName = $_POST['tool_consumer_instance_name'];
  1012. $doSaveConsumer = true;
  1013. }
  1014. }
  1015. if (isset($_POST['tool_consumer_info_product_family_code'])) {
  1016. $version = $_POST['tool_consumer_info_product_family_code'];
  1017. if (isset($_POST['tool_consumer_info_version'])) {
  1018. $version .= "-{$_POST['tool_consumer_info_version']}";
  1019. }
  1020. // do not delete any existing consumer version if none is passed
  1021. if ($this->consumer->consumerVersion !== $version) {
  1022. $this->consumer->consumerVersion = $version;
  1023. $doSaveConsumer = true;
  1024. }
  1025. } else if (isset($_POST['ext_lms']) && ($this->consumer->consumerName !== $_POST['ext_lms'])) {
  1026. $this->consumer->consumerVersion = $_POST['ext_lms'];
  1027. $doSaveConsumer = true;
  1028. }
  1029. if (isset($_POST['tool_consumer_instance_guid'])) {
  1030. if (is_null($this->consumer->consumerGuid)) {
  1031. $this->consumer->consumerGuid = $_POST['tool_consumer_instance_guid'];
  1032. $doSaveConsumer = true;
  1033. } else if (!$this->consumer->protected) {
  1034. $doSaveConsumer = ($this->consumer->consumerGuid !== $_POST['tool_consumer_instance_guid']);
  1035. if ($doSaveConsumer) {
  1036. $this->consumer->consumerGuid = $_POST['tool_consumer_instance_guid'];
  1037. }
  1038. }
  1039. }
  1040. if (isset($_POST['launch_presentation_css_url'])) {
  1041. if ($this->consumer->cssPath !== $_POST['launch_presentation_css_url']) {
  1042. $this->consumer->cssPath = $_POST['launch_presentation_css_url'];
  1043. $doSaveConsumer = true;
  1044. }
  1045. } else if (isset($_POST['ext_launch_presentation_css_url']) &&
  1046. ($this->consumer->cssPath !== $_POST['ext_launch_presentation_css_url'])) {
  1047. $this->consumer->cssPath = $_POST['ext_launch_presentation_css_url'];
  1048. $doSaveConsumer = true;
  1049. } else if (!empty($this->consumer->cssPath)) {
  1050. $this->consumer->cssPath = null;
  1051. $doSaveConsumer = true;
  1052. }
  1053. }
  1054. // Persist changes to consumer
  1055. if ($doSaveConsumer) {
  1056. $this->consumer->save();
  1057. }
  1058. if ($this->ok && isset($this->context)) {
  1059. $this->context->save();
  1060. }
  1061. if ($this->ok && isset($this->resourceLink)) {
  1062. // Check if a share arrangement is in place for this resource link
  1063. $this->ok = $this->checkForShare();
  1064. // Persist changes to resource link
  1065. $this->resourceLink->save();
  1066. // Save the user instance
  1067. if (isset($_POST['lis_result_sourcedid'])) {
  1068. if ($this->user->ltiResultSourcedId !== $_POST['lis_result_sourcedid']) {
  1069. $this->user->ltiResultSourcedId = $_POST['lis_result_sourcedid'];
  1070. $this->user->save();
  1071. }
  1072. } else if (!empty($this->user->ltiResultSourcedId)) {
  1073. $this->user->ltiResultSourcedId = '';
  1074. $this->user->save();
  1075. }
  1076. }
  1077. return $this->ok;
  1078. }
  1079. /**
  1080. * Check if a share arrangement is in place.
  1081. *
  1082. * @return boolean True if no error is reported
  1083. */
  1084. private function checkForShare()
  1085. {
  1086. $ok = true;
  1087. $doSaveResourceLink = true;
  1088. $id = $this->resourceLink->primaryResourceLinkId;
  1089. $shareRequest = isset($_POST['custom_share_key']) && !empty($_POST['custom_share_key']);
  1090. if ($shareRequest) {
  1091. if (!$this->allowSharing) {
  1092. $ok = false;
  1093. $this->reason = 'Your sharing request has been refused because sharing is not being permitted.';
  1094. } else {
  1095. // Check if this is a new share key
  1096. $shareKey = new ResourceLinkShareKey($this->resourceLink, $_POST['custom_share_key']);
  1097. if (!is_null($shareKey->primaryConsumerKey) && !is_null($shareKey->primaryResourceLinkId)) {
  1098. // Update resource link with sharing primary resource link details
  1099. $key = $shareKey->primaryConsumerKey;
  1100. $id = $shareKey->primaryResourceLinkId;
  1101. $ok = ($key !== $this->consumer->getKey()) || ($id != $this->resourceLink->getId());
  1102. if ($ok) {
  1103. $this->resourceLink->primaryConsumerKey = $key;
  1104. $this->resourceLink->primaryResourceLinkId = $id;
  1105. $this->resourceLink->shareApproved = $shareKey->autoApprove;
  1106. $ok = $this->resourceLink->save();
  1107. if ($ok) {
  1108. $doSaveResourceLink = false;
  1109. $this->user->getResourceLink()->primaryConsumerKey = $key;
  1110. $this->user->getResourceLink()->primaryResourceLinkId = $id;
  1111. $this->user->getResourceLink()->shareApproved = $shareKey->autoApprove;
  1112. $this->user->getResourceLink()->updated = time();
  1113. // Remove share key
  1114. $shareKey->delete();
  1115. } else {
  1116. $this->reason = 'An error occurred initialising your share arrangement.';
  1117. }
  1118. } else {
  1119. $this->reason = 'It is not possible to share your resource link with yourself.';
  1120. }
  1121. }
  1122. if ($ok) {
  1123. $ok = !is_null($key);
  1124. if (!$ok) {
  1125. $this->reason = 'You have requested to share a resource link but none is available.';
  1126. } else {
  1127. $ok = (!is_null($this->user->getResourceLink()->shareApproved) && $this->user->getResourceLink()->shareApproved);
  1128. if (!$ok) {
  1129. $this->reason = 'Your share request is waiting to be approved.';
  1130. }
  1131. }
  1132. }
  1133. }
  1134. } else {
  1135. // Check no share is in place
  1136. $ok = is_null($id);
  1137. if (!$ok) {
  1138. $this->reason = 'You have not requested to share a resource link but an arrangement is currently in place.';
  1139. }
  1140. }
  1141. // Look up primary resource link
  1142. if ($ok && !is_null($id)) {
  1143. $consumer = new ToolConsumer($key, $this->dataConnector);
  1144. $ok = !is_null($consumer->created);
  1145. if ($ok) {
  1146. $resourceLink = ResourceLink::fromConsumer($consumer, $id);
  1147. $ok = !is_null($resourceLink->created);
  1148. }
  1149. if ($ok) {
  1150. if ($doSaveResourceLink) {
  1151. $this->resourceLink->save();
  1152. }
  1153. $this->resourceLink = $resourceLink;
  1154. } else {
  1155. $this->reason = 'Unable to load resource link being shared.';
  1156. }
  1157. }
  1158. return $ok;
  1159. }
  1160. /**
  1161. * Validate a parameter value from an array of permitted values.
  1162. *
  1163. * @return boolean True if value is valid
  1164. */
  1165. private function checkValue($value, $values, $reason)
  1166. {
  1167. $ok = in_array($value, $values);
  1168. if (!$ok && !empty($reason)) {

Large files files are truncated, but you can click here to view the full file