LTI Integration Library 4.10.3
PHP class library for building LTI integrations
 
Loading...
Searching...
No Matches
System.php
1<?php
2
3namespace ceLTIc\LTI;
4
8use ceLTIc\LTI\OAuth;
13
21trait System
22{
23
29 public $ok = true;
30
36 public $ltiVersion = null;
37
43 public $name = null;
44
50 public $secret = null;
51
57 public $signatureMethod = 'HMAC-SHA1';
58
64 public $encryptionMethod = '';
65
71 public $dataConnector = null;
72
81 public $rsaKey = null;
82
88 public $requiredScopes = array();
89
95 public $kid = null;
96
102 public $jku = null;
103
109 public $reason = null;
110
116 public $details = array();
117
123 public $warnings = array();
124
130 public $debugMode = false;
131
137 public $enabled = false;
138
144 public $enableFrom = null;
145
151 public $enableUntil = null;
152
158 public $lastAccess = null;
159
165 public $created = null;
166
172 public $updated = null;
173
179 protected $jwt = null;
180
186 protected $rawParameters = null;
187
193 protected $messageParameters = null;
194
200 private $id = null;
201
207 private $key = null;
208
214 private $settings = null;
215
221 private $settingsChanged = false;
222
228 public function getRecordId()
229 {
230 return $this->id;
231 }
232
238 public function setRecordId($id)
239 {
240 $this->id = $id;
241 }
242
248 public function getKey()
249 {
250 return $this->key;
251 }
252
258 public function setKey($key)
259 {
260 $this->key = $key;
261 }
262
271 public function getSetting($name, $default = '')
272 {
273 if (array_key_exists($name, $this->settings)) {
274 $value = $this->settings[$name];
275 } else {
276 $value = $default;
277 }
278
279 return $value;
280 }
281
288 public function setSetting($name, $value = null)
289 {
290 $old_value = $this->getSetting($name);
291 if ($value !== $old_value) {
292 if (!empty($value)) {
293 $this->settings[$name] = $value;
294 } else {
295 unset($this->settings[$name]);
296 }
297 $this->settingsChanged = true;
298 }
299 }
300
306 public function getSettings()
307 {
308 return $this->settings;
309 }
310
316 public function setSettings($settings)
317 {
318 $this->settings = $settings;
319 }
320
326 public function saveSettings()
327 {
328 if ($this->settingsChanged) {
329 $ok = $this->save();
330 } else {
331 $ok = true;
332 }
333
334 return $ok;
335 }
336
342 public function hasJwt()
343 {
344 return !empty($this->jwt) && $this->jwt->hasJwt();
345 }
346
352 public function getJwt()
353 {
354 return $this->jwt;
355 }
356
362 public function getRawParameters()
363 {
364 if (is_null($this->rawParameters)) {
365 $this->rawParameters = OAuth\OAuthUtil::parse_parameters(file_get_contents(OAuth\OAuthRequest::$POST_INPUT));
366 }
367
368 return $this->rawParameters;
369 }
370
378 public function getMessageClaims($fullyQualified = false)
379 {
380 $messageClaims = null;
381 if (!is_null($this->messageParameters)) {
382 $messageParameters = $this->messageParameters;
383 $messageType = '';
384 if (!empty($messageParameters['lti_message_type'])) {
385 if (array_key_exists($messageParameters['lti_message_type'], Util::MESSAGE_TYPE_MAPPING)) {
386 $messageParameters['lti_message_type'] = Util::MESSAGE_TYPE_MAPPING[$messageParameters['lti_message_type']];
387 }
388 $messageType = $messageParameters['lti_message_type'];
389 }
390 if (!empty($messageParameters['accept_media_types'])) {
391 $mediaTypes = array_map('trim', explode(',', $messageParameters['accept_media_types']));
392 $mediaTypes = array_filter($mediaTypes);
393 $types = array();
394 if (!empty($messageParameters['accept_types'])) {
395 $types = array_map('trim', explode(',', $this->messageParameters['accept_types']));
396 $types = array_filter($types);
397 foreach ($mediaTypes as $mediaType) {
398 if (strpos($mediaType, 'application/vnd.ims.lti.') === 0) {
399 unset($mediaTypes[array_search($mediaType, $mediaTypes)]);
400 }
401 }
402 $messageParameters['accept_media_types'] = implode(',', $mediaTypes);
403 } else {
404 foreach ($mediaTypes as $mediaType) {
405 if ($mediaType === Item::LTI_LINK_MEDIA_TYPE) {
406 unset($mediaTypes[array_search(Item::LTI_LINK_MEDIA_TYPE, $mediaTypes)]);
407 $messageParameters['accept_media_types'] = implode(',', $mediaTypes);
408 $types[] = Item::TYPE_LTI_LINK;
409 } elseif ($mediaType === Item::LTI_ASSIGNMENT_MEDIA_TYPE) {
410 unset($mediaTypes[array_search(Item::LTI_ASSIGNMENT_MEDIA_TYPE, $mediaTypes)]);
411 $messageParameters['accept_media_types'] = implode(',', $mediaTypes);
412 $types[] = Item::TYPE_LTI_ASSIGNMENT;
413 } elseif (substr($mediaType, 0, 6) === 'image/') {
414 $types[] = 'image';
415 $types[] = 'link';
416 $types[] = 'file';
417 } elseif ($mediaType === 'text/html') {
418 $types[] = 'html';
419 $types[] = 'link';
420 $types[] = 'file';
421 } elseif ($mediaType === '*/*') {
422 $types[] = 'html';
423 $types[] = 'image';
424 $types[] = 'file';
425 $types[] = 'link';
426 } else {
427 $types[] = 'file';
428 }
429 }
430 $types = array_unique($types);
431 $messageParameters['accept_types'] = implode(',', $types);
432 }
433 }
434 if (!empty($messageParameters['accept_presentation_document_targets'])) {
435 $documentTargets = array_map('trim', explode(',', $messageParameters['accept_presentation_document_targets']));
436 $documentTargets = array_filter($documentTargets);
437 $targets = array();
438 foreach ($documentTargets as $documentTarget) {
439 switch ($documentTarget) {
440 case 'frame':
441 case 'popup':
442 case 'overlay':
443 case 'none':
444 break;
445 default:
446 $targets[] = $documentTarget;
447 break;
448 }
449 }
450 $targets = array_unique($targets);
451 $messageParameters['accept_presentation_document_targets'] = implode(',', $targets);
452 }
453 $messageClaims = array();
454 if (!empty($messageParameters['oauth_consumer_key'])) {
455 $messageClaims['aud'] = array($messageParameters['oauth_consumer_key']);
456 }
457 foreach ($messageParameters as $key => $value) {
458 $ok = true;
459 if (array_key_exists($key, Util::JWT_CLAIM_MAPPING)) {
460 if (array_key_exists("{$key}.{$messageType}", Util::JWT_CLAIM_MAPPING)) {
461 $mapping = Util::JWT_CLAIM_MAPPING["{$key}.{$messageType}"];
462 } else {
463 $mapping = Util::JWT_CLAIM_MAPPING[$key];
464 }
465 if (isset($mapping['isObject']) && $mapping['isObject']) {
466 $value = Util::jsonDecode($value);
467 } elseif (isset($mapping['isArray']) && $mapping['isArray']) {
468 $value = array_map('trim', explode(',', $value));
469 $value = array_filter($value);
470 sort($value);
471 } elseif (isset($mapping['isBoolean']) && $mapping['isBoolean']) {
472 $value = (is_bool($value)) ? $value : $value === 'true';
473 } elseif (isset($mapping['isInteger']) && $mapping['isInteger']) {
474 $value = intval($value);
475 } elseif (is_bool($value)) {
476 $value = ($value) ? 'true' : 'false';
477 } else {
478 $value = strval($value);
479 }
480 $group = '';
481 $claim = Util::JWT_CLAIM_PREFIX;
482 if (!empty($mapping['suffix'])) {
483 $claim .= "-{$mapping['suffix']}";
484 }
485 $claim .= '/claim/';
486 if (is_null($mapping['group'])) {
487 $claim = $mapping['claim'];
488 } elseif (empty($mapping['group'])) {
489 $claim .= $mapping['claim'];
490 } else {
491 $group = $claim . $mapping['group'];
492 $claim = $mapping['claim'];
493 }
494 } elseif (substr($key, 0, 7) === 'custom_') {
495 $group = Util::JWT_CLAIM_PREFIX . '/claim/custom';
496 $claim = substr($key, 7);
497 } elseif (substr($key, 0, 4) === 'ext_') {
498 if ($key === 'ext_d2l_username') {
499 $group = 'http://www.brightspace.com';
500 $claim = 'username';
501 } else {
502 $group = Util::JWT_CLAIM_PREFIX . '/claim/ext';
503 $claim = substr($key, 4);
504 }
505 } elseif (substr($key, 0, 7) === 'lti1p1_') {
506 $group = Util::JWT_CLAIM_PREFIX . '/claim/lti1p1';
507 $claim = substr($key, 7);
508 if (empty($value)) {
509 $value = null;
510 } else {
511 $json = Util::jsonDecode($value);
512 if (!is_null($json)) {
513 $value = $json;
514 }
515 }
516 } else {
517 $ok = false;
518 }
519 if ($ok) {
520 if ($fullyQualified) {
521 if (empty($group)) {
522 $messageClaims = array_merge($messageClaims, self::fullyQualifyClaim($claim, $value));
523 } else {
524 $messageClaims = array_merge($messageClaims, self::fullyQualifyClaim("{$group}/{$claim}", $value));
525 }
526 } elseif (empty($group)) {
527 $messageClaims[$claim] = $value;
528 } else {
529 $messageClaims[$group][$claim] = $value;
530 }
531 }
532 }
533 if (!empty($messageParameters['unmapped_claims'])) {
534 $claims = Util::jsonDecode($messageParameters['unmapped_claims']);
535 foreach ($claims as $claim => $value) {
536 if ($fullyQualified) {
537 $messageClaims = array_merge($messageClaims, self::fullyQualifyClaim($claim, $value));
538 } elseif (!is_object($value)) {
539 $messageClaims[$claim] = $value;
540 } elseif (!isset($messageClaims[$claim])) {
541 $messageClaims[$claim] = $value;
542 } else {
543 $objVars = get_object_vars($value);
544 foreach ($objVars as $attrName => $attrValue) {
545 if (is_object($messageClaims[$claim])) {
546 $messageClaims[$claim]->{$attrName} = $attrValue;
547 } else {
548 $messageClaims[$claim][$attrName] = $attrValue;
549 }
550 }
551 }
552 }
553 }
554 }
555
556 return $messageClaims;
557 }
558
568 public static function parseRoles($roles, $ltiVersion = Util::LTI_VERSION1, $addPrincipalRole = false)
569 {
570 if (!is_array($roles)) {
571 $roles = array_map('trim', explode(',', $roles));
572 $roles = array_filter($roles);
573 }
574 $parsedRoles = array();
575 foreach ($roles as $role) {
576 $role = trim($role);
577 if ((substr($role, 0, 4) !== 'urn:') &&
578 (substr($role, 0, 7) !== 'http://') && (substr($role, 0, 8) !== 'https://')) {
579 switch ($ltiVersion) {
581 $role = str_replace('#', '/', $role);
582 $role = "urn:lti:role:ims/lis/{$role}";
583 break;
586 $pos = strrpos($role, '#');
587 if ($pos === false) {
588 $sep = '#';
589 } else {
590 $sep = '/';
591 }
592 $role = "http://purl.imsglobal.org/vocab/lis/v2/membership{$sep}{$role}";
593 break;
594 }
595 }
596 $systemRoles = array(
597 'AccountAdmin',
598 'Administrator',
599 'Creator',
600 'None',
601 'SysAdmin',
602 'SysSupport',
603 'User');
604 $institutionRoles = array(
605// 'Administrator', // System Administrator role takes precedence
606 'Alumni',
607 'Faculty',
608 'Guest',
609 'Instructor',
610 'Learner',
611 'Member',
612 'Mentor',
613 'None',
614 'Observer',
615 'Other',
616 'ProspectiveStudent',
617 'Staff',
618 'Student'
619 );
620 switch ($ltiVersion) {
622 if (in_array(substr($role, 0, 53),
623 array('http://purl.imsglobal.org/vocab/lis/v2/system/person#',
624 'http://purl.imsglobal.org/vocab/lis/v2/system/person/'))) {
625 $role = 'urn:lti:sysrole:ims/lis/' . substr($role, 53);
626 } elseif (in_array(substr($role, 0, 58),
627 array('http://purl.imsglobal.org/vocab/lis/v2/institution/person#',
628 'http://purl.imsglobal.org/vocab/lis/v2/institution/person/'))) {
629 $role = 'urn:lti:instrole:ims/lis/' . substr($role, 58);
630 } elseif (substr($role, 0, 50) === 'http://purl.imsglobal.org/vocab/lis/v2/membership#') {
631 $principalRole = substr($role, 50);
632 if (($principalRole === 'Instructor') &&
633 (!empty(preg_grep('/^http:\/\/purl.imsglobal.org\/vocab\/lis\/v2\/membership\/Instructor#TeachingAssistant.*$/',
634 $roles)) ||
635 !empty(preg_grep('/^Instructor#TeachingAssistant.*$/', $roles)))) {
636 $role = '';
637 } elseif (!empty(preg_grep("/^http:\/\/purl.imsglobal.org\/vocab\/lis\/v2\/membership\/{$principalRole}#.*$/",
638 $roles)) ||
639 !empty(preg_grep('/^{$principalRole}#.*$/', $roles))) {
640 $role = '';
641 } else {
642 $role = "urn:lti:role:ims/lis/{$principalRole}";
643 }
644 } elseif (substr($role, 0, 50) === 'http://purl.imsglobal.org/vocab/lis/v2/membership/') {
645 $subroles = explode('#', substr($role, 50));
646 if (count($subroles) === 2) {
647 if (($subroles[0] === 'Instructor') && ($subroles[1] === 'TeachingAssistant')) {
648 $role = 'urn:lti:role:ims/lis/TeachingAssistant';
649 } elseif (($subroles[0] === 'Instructor') && (substr($subroles[1], 0, 17) === 'TeachingAssistant')) {
650 $role = "urn:lti:role:ims/lis/TeachingAssistant#{$subroles[1]}";
651 } else {
652 $role = "urn:lti:role:ims/lis/{$subroles[0]}/{$subroles[1]}";
653 }
654 } else {
655 $role = 'urn:lti:role:ims/lis/' . substr($role, 50);
656 }
657 } elseif (in_array(substr($role, 0, 46),
658 array('http://purl.imsglobal.org/vocab/lis/v2/person#',
659 'http://purl.imsglobal.org/vocab/lis/v2/person/'))) {
660 if (in_array(substr($role, 46), $systemRoles)) {
661 $role = 'urn:lti:sysrole:ims/lis/' . substr($role, 46);
662 } elseif (in_array(substr($role, 46), $institutionRoles)) {
663 $role = 'urn:lti:instrole:ims/lis/' . substr($role, 46);
664 }
665 } elseif (strpos($role, 'Instructor#TeachingAssistant') !== false) {
666 if (substr($role, -28) === 'Instructor#TeachingAssistant') {
667 $role = str_replace('Instructor#', '', $role);
668 } else {
669 $role = str_replace('Instructor#', 'TeachingAssistant/', $role);
670 }
671 } elseif ((substr($role, -10) === 'Instructor') &&
672 !empty(preg_grep('/^http:\/\/purl.imsglobal.org\/vocab\/lis\/v2\/membership\/Instructor#TeachingAssistant.*$/',
673 $roles))) {
674 $role = '';
675 }
676 $role = str_replace('#', '/', $role);
677 break;
679 $prefix = '';
680 if (substr($role, 0, 24) === 'urn:lti:sysrole:ims/lis/') {
681 $prefix = 'http://purl.imsglobal.org/vocab/lis/v2/person';
682 $role = substr($role, 24);
683 } elseif (substr($role, 0, 25) === 'urn:lti:instrole:ims/lis/') {
684 $prefix = 'http://purl.imsglobal.org/vocab/lis/v2/person';
685 $role = substr($role, 25);
686 } elseif (substr($role, 0, 21) === 'urn:lti:role:ims/lis/') {
687 $prefix = 'http://purl.imsglobal.org/vocab/lis/v2/membership';
688 $subroles = explode('/', substr($role, 21));
689 if (count($subroles) === 2) {
690 if (($subroles[0] === 'Instructor') && ($subroles[1] === 'TeachingAssistant')) {
691 $role = 'TeachingAssistant';
692 } elseif (($subroles[0] === 'Instructor') && (substr($subroles[1], 0, 17) === 'TeachingAssistant')) {
693 $role = "TeachingAssistant#{$subroles[1]}";
694 } else {
695 $role = "{$subroles[0]}#{$subroles[1]}";
696 }
697 } elseif ((count($subroles) === 1) && (!empty(preg_grep("/^http:\/\/purl.imsglobal.org\/vocab\/lis\/v2\/membership\/{$subroles[0]}#.*$/",
698 $roles)) ||
699 !empty(preg_grep('/^{$subroles[0]#.*$/', $roles)))) {
700 $role = '';
701 } else {
702 $role = substr($role, 21);
703 }
704 } elseif (in_array(substr($role, 0, 53),
705 array('http://purl.imsglobal.org/vocab/lis/v2/system/person#',
706 'http://purl.imsglobal.org/vocab/lis/v2/system/person/'))) {
707 $prefix = 'http://purl.imsglobal.org/vocab/lis/v2/person';
708 $role = substr($role, 53);
709 } elseif (in_array(substr($role, 0, 58),
710 array('http://purl.imsglobal.org/vocab/lis/v2/institution/person#',
711 'http://purl.imsglobal.org/vocab/lis/v2/institution/person/'))) {
712 $prefix = 'http://purl.imsglobal.org/vocab/lis/v2/person';
713 $role = substr($role, 58);
714 } elseif (substr($role, 0, 50) === 'http://purl.imsglobal.org/vocab/lis/v2/membership#') {
715 $prefix = 'http://purl.imsglobal.org/vocab/lis/v2/membership';
716 $principalRole = substr($role, 50);
717 $principalRole2 = str_replace('/', '\\/', $principalRole);
718 if (($principalRole === 'Instructor') &&
719 (!empty(preg_grep('/^http:\/\/purl.imsglobal.org\/vocab\/lis\/v2\/membership\/Instructor#TeachingAssistant.*$/',
720 $roles)) ||
721 !empty(preg_grep('/^Instructor#TeachingAssistant.*$/', $roles)))) {
722 $role = '';
723 } elseif (!empty(preg_grep("/^http:\/\/purl.imsglobal.org\/vocab\/lis\/v2\/membership\/{$principalRole2}#.*$/",
724 $roles)) ||
725 !empty(preg_grep('/^{$principalRole2}#.*$/', $roles))) {
726 $role = '';
727 } else {
728 $role = $principalRole;
729 }
730 } elseif (substr($role, 0, 50) === 'http://purl.imsglobal.org/vocab/lis/v2/membership/') {
731 $prefix = 'http://purl.imsglobal.org/vocab/lis/v2/membership';
732 $subroles = explode('#', substr($role, 50));
733 if (count($subroles) === 2) {
734 if (($subroles[0] === 'Instructor') && ($subroles[1] === 'TeachingAssistant')) {
735 $role = 'TeachingAssistant';
736 } elseif (($subroles[0] === 'Instructor') && (substr($subroles[1], 0, 17) === 'TeachingAssistant')) {
737 $role = "TeachingAssistant#{$subroles[1]}";
738 } else {
739 $role = "{$subroles[0]}#{$subroles[1]}";
740 }
741 } else {
742 $role = substr($role, 50);
743 }
744 }
745 if (!empty($role)) {
746 $pos = strrpos($role, '/');
747 if ((strpos($role, '#') !== false) || ($pos !== false)) {
748 $prefix .= '/';
749 if ($pos !== false) {
750 $role = substr($role, 0, $pos) . '#' . substr($role, $pos + 1);
751 }
752 } else {
753 $prefix .= '#';
754 }
755 $role = "{$prefix}{$role}";
756 }
757 break;
759 $prefix = '';
760 if (substr($role, 0, 24) === 'urn:lti:sysrole:ims/lis/') {
761 $prefix = 'http://purl.imsglobal.org/vocab/lis/v2/system/person';
762 $role = substr($role, 24);
763 } elseif (substr($role, 0, 25) === 'urn:lti:instrole:ims/lis/') {
764 $prefix = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person';
765 $role = substr($role, 25);
766 } elseif (substr($role, 0, 21) === 'urn:lti:role:ims/lis/') {
767 $prefix = 'http://purl.imsglobal.org/vocab/lis/v2/membership';
768 $subroles = explode('/', substr($role, 21));
769 if (count($subroles) === 2) {
770 if ($subroles[0] === 'TeachingAssistant') {
771 $role = "Instructor#{$subroles[1]}";
772 if ($addPrincipalRole) {
773 $parsedRoles[] = "{$prefix}#Instructor";
774 }
775 } else {
776 $role = "{$subroles[0]}#{$subroles[1]}";
777 if ($addPrincipalRole) {
778 $parsedRoles[] = "{$prefix}#{$subroles[0]}";
779 }
780 }
781 } elseif ($subroles[0] === 'TeachingAssistant') {
782 $role = 'Instructor#TeachingAssistant';
783 if ($addPrincipalRole) {
784 $parsedRoles[] = "{$prefix}#Instructor";
785 }
786 } else {
787 $role = substr($role, 21);
788 }
789 } elseif (substr($role, 0, 46) === 'http://purl.imsglobal.org/vocab/lis/v2/person#') {
790 if (in_array(substr($role, 46), $systemRoles)) {
791 $prefix = 'http://purl.imsglobal.org/vocab/lis/v2/system/person';
792 } elseif (in_array(substr($role, 46), $institutionRoles)) {
793 $prefix = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person';
794 }
795 $role = substr($role, 46);
796 $pos = strrpos($role, '/');
797 if ($pos !== false) {
798 $role = substr($role, 0, $pos - 1) . '#' . substr($role, $pos + 1);
799 }
800 } elseif (substr($role, 0, 50) === 'http://purl.imsglobal.org/vocab/lis/v2/membership#') {
801 $prefix = 'http://purl.imsglobal.org/vocab/lis/v2/membership';
802 if (substr($role, 50, 18) === 'TeachingAssistant') {
803 $role = 'Instructor#TeachingAssistant';
804 if ($addPrincipalRole) {
805 $parsedRoles[] = "{$prefix}#Instructor";
806 }
807 } else {
808 $role = substr($role, 50);
809 }
810 } elseif (substr($role, 0, 50) === 'http://purl.imsglobal.org/vocab/lis/v2/membership/') {
811 $prefix = 'http://purl.imsglobal.org/vocab/lis/v2/membership';
812 $subroles = explode('#', substr($role, 50));
813 if (count($subroles) === 2) {
814 if ($subroles[0] === 'TeachingAssistant') {
815 $role = "Instructor#{subroles[1]}";
816 if ($addPrincipalRole) {
817 $parsedRoles[] = "{$prefix}#Instructor";
818 }
819 } else {
820 $role = substr($role, 50);
821 if ($addPrincipalRole) {
822 $parsedRoles[] = "{$prefix}#{$subroles[0]}";
823 }
824 }
825 } else {
826 $role = substr($role, 50);
827 }
828 }
829 if (!empty($prefix) && !empty($role)) {
830 $pos = strrpos($role, '/');
831 if ((strpos($role, '#') !== false) || ($pos !== false)) {
832 $prefix .= '/';
833 if ($pos !== false) {
834 $role = substr($role, 0, $pos) . '#' . substr($role, $pos + 1);
835 }
836 } else {
837 $prefix .= '#';
838 }
839 $role = "{$prefix}{$role}";
840 }
841 break;
842 }
843 if (!empty($role)) {
844 $parsedRoles[] = $role;
845 }
846 }
847
848 return array_unique($parsedRoles);
849 }
850
861 public function signParameters($url, $type, $version, $params)
862 {
863 if (!empty($url)) {
864// Add standard parameters
865 $params['lti_version'] = $version;
866 $params['lti_message_type'] = $type;
867// Add signature
868 $params = $this->addSignature($url, $params, 'POST', 'application/x-www-form-urlencoded');
869 }
870
871 return $params;
872 }
873
889 public function signMessage(&$url, $type, $version, $params, $loginHint = null, $ltiMessageHint = null)
890 {
891 if (($this instanceof Platform) && ($this->ltiVersion === Util::LTI_VERSION1P3)) {
892 if (!isset($loginHint) || (strlen($loginHint) <= 0)) {
893 if (isset($params['user_id']) && (strlen($params['user_id']) > 0)) {
894 $loginHint = $params['user_id'];
895 } else {
896 $loginHint = 'Anonymous';
897 }
898 }
899// Add standard parameters
900 $params['lti_version'] = $version;
901 $params['lti_message_type'] = $type;
902 $this->onInitiateLogin($url, $loginHint, $ltiMessageHint, $params);
903
904 $params = array(
905 'iss' => $this->platformId,
906 'target_link_uri' => $url,
907 'login_hint' => $loginHint
908 );
909 if (!is_null($ltiMessageHint)) {
910 $params['lti_message_hint'] = $ltiMessageHint;
911 }
912 if (!empty($this->clientId)) {
913 $params['client_id'] = $this->clientId;
914 }
915 if (!empty($this->deploymentId)) {
916 $params['lti_deployment_id'] = $this->deploymentId;
917 }
918 if (!empty(Tool::$defaultTool)) {
919 $url = Tool::$defaultTool->initiateLoginUrl;
920 }
921 if (!empty(static::$browserStorageFrame)) {
922 if (strpos($url, '?') === false) {
923 $sep = '?';
924 } else {
925 $sep = '&';
926 }
927 $url .= "{$sep}lti_storage_target=" . static::$browserStorageFrame;
928 }
929 } else {
930 $params = $this->signParameters($url, $type, $version, $params);
931 }
932
933 return $params;
934 }
935
948 public function sendMessage($url, $type, $messageParams, $target = '', $userId = null, $hint = null)
949 {
950 $sendParams = $this->signMessage($url, $type, $this->ltiVersion, $messageParams, $userId, $hint);
951 $html = Util::sendForm($url, $sendParams, $target);
952
953 return $html;
954 }
955
966 public function signServiceRequest($url, $method, $type, $data = null)
967 {
968 $header = '';
969 if (!empty($url)) {
970 $header = $this->addSignature($url, $data, $method, $type);
971 }
972
973 return $header;
974 }
975
986 public function doServiceRequest($service, $method, $format, $data)
987 {
988 $header = $this->addSignature($service->endpoint, $data, $method, $format);
989
990// Connect to platform
991 $http = new HttpMessage($service->endpoint, $method, $data, $header);
992// Parse JSON response
993 if ($http->send() && !empty($http->response)) {
994 $http->responseJson = Util::jsonDecode($http->response);
995 $http->ok = !is_null($http->responseJson);
996 }
997
998 return $http;
999 }
1000
1006 public function useOAuth1()
1007 {
1008 return empty($this->signatureMethod) || (substr($this->signatureMethod, 0, 2) !== 'RS');
1009 }
1010
1024 public function addSignature($endpoint, $data, $method = 'POST', $type = null, $nonce = '', $hash = null, $timestamp = null)
1025 {
1026 if ($this->useOAuth1()) {
1027 return $this->addOAuth1Signature($endpoint, $data, $method, $type, $hash, $timestamp);
1028 } else {
1029 return $this->addJWTSignature($endpoint, $data, $method, $type, $nonce, $timestamp);
1030 }
1031 }
1032
1038 public function checkMessage()
1039 {
1040 $ok = $_SERVER['REQUEST_METHOD'] === 'POST';
1041 if (!$ok) {
1042 $this->reason = 'LTI messages must use HTTP POST';
1043 } elseif (!empty($this->jwt) && !empty($this->jwt->hasJwt())) {
1044 $ok = false;
1045 $context = $this->jwt->getClaim('https://purl.imsglobal.org/spec/lti/claim/context');
1046 $resourceLink = $this->jwt->getClaim('https://purl.imsglobal.org/spec/lti/claim/resource_link');
1047 if (is_null($this->messageParameters['oauth_consumer_key']) || (strlen($this->messageParameters['oauth_consumer_key']) <= 0)) {
1048 $this->reason = 'Missing iss claim';
1049 } elseif (empty($this->jwt->getClaim('iat', ''))) {
1050 $this->reason = 'Missing iat claim';
1051 } elseif (empty($this->jwt->getClaim('exp', ''))) {
1052 $this->reason = 'Missing exp claim';
1053 } elseif (intval($this->jwt->getClaim('iat')) > intval($this->jwt->getClaim('exp'))) {
1054 $this->reason = 'iat claim must not have a value greater than exp claim';
1055 } elseif (empty($this->jwt->getClaim('nonce', ''))) {
1056 $this->reason = 'Missing nonce claim';
1057 } elseif (!empty($context) && property_exists($context, 'id') && (empty($context->id) || !is_string($context->id))) {
1058 $this->reason = 'Invalid value for id property in https://purl.imsglobal.org/spec/lti/claim/context claim';
1059 } elseif (!empty($resourceLink) && property_exists($resourceLink, 'id') && (empty($resourceLink->id) || !is_string($resourceLink->id))) {
1060 $this->reason = 'Invalid value for id property in https://purl.imsglobal.org/spec/lti/claim/resource_link claim';
1061 } else {
1062 $ok = true;
1063 }
1064 }
1065// Set signature method from request
1066 if (isset($this->messageParameters['oauth_signature_method'])) {
1067 $this->signatureMethod = $this->messageParameters['oauth_signature_method'];
1068 if (($this instanceof Tool) && !empty($this->platform)) {
1069 $this->platform->signatureMethod = $this->signatureMethod;
1070 }
1071 }
1072// Check all required launch parameters
1073 if ($ok) {
1074 $ok = isset($this->messageParameters['lti_message_type']);
1075 if (!$ok) {
1076 $this->reason = 'Missing lti_message_type parameter.';
1077 }
1078 }
1079 if ($ok) {
1080 $ok = isset($this->messageParameters['lti_version']) && in_array($this->messageParameters['lti_version'],
1082 if (!$ok) {
1083 $this->reason = 'Invalid or missing lti_version parameter.';
1084 }
1085 }
1086
1087 return $ok;
1088 }
1089
1095 public function verifySignature()
1096 {
1097 $ok = false;
1098 $key = $this->key;
1099 if (!empty($key)) {
1100 $secret = $this->secret;
1101 } elseif (($this instanceof Tool) && !empty($this->platform)) {
1102 $key = $this->platform->getKey();
1103 $secret = $this->platform->secret;
1104 } elseif (($this instanceof Platform) && !empty(Tool::$defaultTool)) {
1105 $key = Tool::$defaultTool->getKey();
1106 $secret = Tool::$defaultTool->secret;
1107 }
1108 if ($this instanceof Tool) {
1109 $platform = $this->platform;
1110 $publicKey = $this->platform->rsaKey;
1111 $jku = $this->platform->jku;
1112 } else {
1113 $platform = $this;
1114 if (!empty(Tool::$defaultTool)) {
1115 $publicKey = Tool::$defaultTool->rsaKey;
1116 $jku = Tool::$defaultTool->jku;
1117 } else {
1118 $publicKey = $this->rsaKey;
1119 $jku = $this->jku;
1120 }
1121 }
1122 if (empty($this->jwt) || empty($this->jwt->hasJwt())) { // OAuth-signed message
1123 try {
1124 $store = new OAuthDataStore($this);
1125 $server = new OAuth\OAuthServer($store);
1127 $server->add_signature_method($method);
1129 $server->add_signature_method($method);
1131 $server->add_signature_method($method);
1133 $server->add_signature_method($method);
1135 $server->add_signature_method($method);
1136 $request = OAuth\OAuthRequest::from_request();
1137 if (isset($request->get_parameters()['_new_window']) && !isset($this->messageParameters['_new_window'])) {
1138 $request->unset_parameter('_new_window');
1139 }
1140 $server->verify_request($request);
1141 $ok = true;
1142 } catch (\Exception $e) {
1143 if (empty($this->reason)) {
1144 $oauthConsumer = new OAuth\OAuthConsumer($key, $secret);
1145 $signature = $request->build_signature($method, $oauthConsumer, false);
1146 if ($this->debugMode) {
1147 $this->reason = $e->getMessage();
1148 }
1149 if (empty($this->reason)) {
1150 $this->reason = 'OAuth signature check failed - perhaps an incorrect secret or timestamp.';
1151 }
1152 $this->details[] = "Shared secret: '{$secret}'";
1153 $this->details[] = 'Current timestamp: ' . time();
1154 $this->details[] = "Expected signature: {$signature}";
1155 $this->details[] = "Base string: {$request->base_string}";
1156 }
1157 }
1158 } else { // JWT-signed message
1159 $nonce = new PlatformNonce($platform, $this->jwt->getClaim('nonce'));
1160 $ok = !$nonce->load();
1161 if ($ok) {
1162 $ok = $nonce->save();
1163 }
1164 if (!$ok) {
1165 $this->reason = 'Invalid nonce.';
1166 } elseif (!empty($publicKey) || !empty($jku) || Jwt::$allowJkuHeader) {
1167 $ok = $this->jwt->verify($publicKey, $jku);
1168 if (!$ok) {
1169 $this->reason = 'JWT signature check failed - perhaps an invalid public key or timestamp';
1170 }
1171 } else {
1172 $ok = false;
1173 $this->reason = 'Unable to verify JWT signature as neither a public key nor a JSON Web Key URL is specified';
1174 }
1175 }
1176
1177 return $ok;
1178 }
1179
1180###
1181### PRIVATE METHODS
1182###
1183
1191 private function parseMessage($strictMode, $disableCookieCheck, $generateWarnings)
1192 {
1193 if (is_null($this->messageParameters)) {
1194 $this->getRawParameters();
1195 if (isset($this->rawParameters['id_token']) || isset($this->rawParameters['JWT'])) { // JWT-signed message
1196 try {
1197 $this->jwt = Jwt::getJwtClient();
1198 if (isset($this->rawParameters['id_token'])) {
1199 $this->ok = $this->jwt->load($this->rawParameters['id_token'], $this->rsaKey);
1200 } else {
1201 $this->ok = $this->jwt->load($this->rawParameters['JWT'], $this->rsaKey);
1202 }
1203 if (!$this->ok) {
1204 $this->reason = 'Message does not contain a valid JWT';
1205 } else {
1206 $this->ok = $this->jwt->hasClaim('iss') && $this->jwt->hasClaim('aud') && $this->jwt->hasClaim('nonce') &&
1207 $this->jwt->hasClaim(Util::JWT_CLAIM_PREFIX . '/claim/deployment_id');
1208 if ($this->ok) {
1209 $iss = $this->jwt->getClaim('iss');
1210 $aud = $this->jwt->getClaim('aud');
1211 $deploymentId = $this->jwt->getClaim(Util::JWT_CLAIM_PREFIX . '/claim/deployment_id');
1212 $this->ok = !empty($iss) && !empty($aud) && !empty($deploymentId);
1213 if (!$this->ok) {
1214 $this->reason = 'iss, aud and/or deployment_id claim is empty';
1215 } elseif (is_array($aud)) {
1216 if ($this->jwt->hasClaim('azp')) {
1217 $this->ok = !empty($this->jwt->getClaim('azp'));
1218 if (!$this->ok) {
1219 $this->reason = 'azp claim is empty';
1220 } else {
1221 $this->ok = in_array($this->jwt->getClaim('azp'), $aud);
1222 if ($this->ok) {
1223 $aud = $this->jwt->getClaim('azp');
1224 } else {
1225 $this->reason = 'azp claim value is not included in aud claim';
1226 }
1227 }
1228 } else {
1229 $aud = $aud[0];
1230 $this->ok = !empty($aud);
1231 if (!$this->ok) {
1232 $this->reason = 'First element of aud claim is empty';
1233 }
1234 }
1235 } elseif ($this->jwt->hasClaim('azp')) {
1236 $this->ok = $this->jwt->getClaim('azp') === $aud;
1237 if (!$this->ok) {
1238 $this->reason = 'aud claim does not match the azp claim';
1239 }
1240 }
1241 if ($this->ok) {
1242 if ($this instanceof Tool) {
1243 $this->platform = Platform::fromPlatformId($iss, $aud, $deploymentId, $this->dataConnector);
1244 $this->platform->platformId = $iss;
1245 if (isset($this->rawParameters['id_token'])) {
1246 $this->ok = !empty($this->rawParameters['state']);
1247 if ($this->ok) {
1248 $state = $this->rawParameters['state'];
1249 $parts = explode('.', $state);
1250 if (!empty(session_id()) && (count($parts) > 1) && (session_id() !== $parts[1]) &&
1251 ($parts[1] !== 'platformStorage')) { // Reset to original session
1252 session_abort();
1253 session_id($parts[1]);
1254 session_start();
1255 $this->onResetSessionId();
1256 }
1257 $usePlatformStorage = (substr($state, -16) === '.platformStorage');
1258 if ($usePlatformStorage) {
1259 $state = substr($state, 0, -16);
1260 }
1261 $this->onAuthenticate($state, $this->jwt->getClaim('nonce'), $usePlatformStorage);
1262 if (!$disableCookieCheck) {
1263 if (empty($_COOKIE) && !isset($_POST['_new_window'])) { // Reopen in a new window
1265 $_POST['_new_window'] = '';
1266 echo Util::sendForm($_SERVER['REQUEST_URI'], $_POST, '_blank');
1267 exit;
1268 }
1269 Util::setTestCookie(true);
1270 }
1271 } else {
1272 $this->reason = 'state parameter is missing';
1273 }
1274 if ($this->ok) {
1275 $nonce = new PlatformNonce($this->platform, $state);
1276 $this->ok = $nonce->load();
1277 if (!$this->ok) {
1278 $platform = Platform::fromPlatformId($iss, $aud, null, $this->dataConnector);
1279 $nonce = new PlatformNonce($platform, $state);
1280 $this->ok = $nonce->load();
1281 }
1282 if (!$this->ok) {
1283 $platform = Platform::fromPlatformId($iss, null, null, $this->dataConnector);
1284 $nonce = new PlatformNonce($platform, $state);
1285 $this->ok = $nonce->load();
1286 }
1287 if ($this->ok) {
1288 $this->ok = $nonce->delete();
1289 }
1290 if (!$this->ok) {
1291 $this->reason = 'state parameter is invalid or has expired';
1292 }
1293 }
1294 }
1295 }
1296 $this->messageParameters = array();
1297 if ($this->ok) {
1298 $this->messageParameters['oauth_consumer_key'] = $aud;
1299 $this->messageParameters['oauth_signature_method'] = $this->jwt->getHeader('alg');
1300 $this->parseClaims($strictMode, $generateWarnings);
1301 }
1302 }
1303 } else {
1304 $this->reason = 'iss, aud, deployment_id and/or nonce claim not found';
1305 }
1306 }
1307 } catch (\Exception $e) {
1308 $this->ok = false;
1309 $this->reason = 'Message does not contain a valid JWT';
1310 }
1311 } elseif (isset($this->rawParameters['error'])) { // Error with JWT-signed message
1312 $this->ok = false;
1313 $this->reason = $this->rawParameters['error'];
1314 if (!empty($this->rawParameters['error_description'])) {
1315 $this->reason .= ": {$this->rawParameters['error_description']}";
1316 }
1317 } else { // OAuth
1318 if ($this instanceof Tool) {
1319 if (isset($this->rawParameters['oauth_consumer_key'])) {
1320 $this->platform = Platform::fromConsumerKey($this->rawParameters['oauth_consumer_key'], $this->dataConnector);
1321 }
1322 if (isset($this->rawParameters['tool_state'])) { // Relaunch?
1323 $state = $this->rawParameters['tool_state'];
1324 if (!$disableCookieCheck) {
1325 $parts = explode('.', $state);
1326 if (empty($_COOKIE) && !isset($_POST['_new_window'])) { // Reopen in a new window
1328 $_POST['_new_window'] = '';
1329 echo Util::sendForm($_SERVER['REQUEST_URI'], $_POST, '_blank');
1330 exit;
1331 } elseif (!empty(session_id()) && (count($parts) > 1) && (session_id() !== $parts[1])) { // Reset to original session
1332 session_abort();
1333 session_id($parts[1]);
1334 session_start();
1335 $this->onResetSessionId();
1336 }
1337 unset($this->rawParameters['_new_window']);
1338 Util::setTestCookie(true);
1339 }
1340 $nonce = new PlatformNonce($this->platform, $state);
1341 $this->ok = $nonce->load();
1342 if (!$this->ok) {
1343 $this->reason = "Invalid tool_state parameter: '{$state}'";
1344 }
1345 }
1346 }
1347 $this->messageParameters = $this->rawParameters;
1348 }
1349 }
1350 }
1351
1358 private function parseClaims($strictMode, $generateWarnings)
1359 {
1360 $payload = Util::cloneObject($this->jwt->getPayload());
1361 $errors = array();
1362 foreach (Util::JWT_CLAIM_MAPPING as $key => $mapping) {
1363 $claim = Util::JWT_CLAIM_PREFIX;
1364 if (!empty($mapping['suffix'])) {
1365 $claim .= "-{$mapping['suffix']}";
1366 }
1367 $claim .= '/claim/';
1368 if (is_null($mapping['group'])) {
1369 $claim = $mapping['claim'];
1370 } elseif (empty($mapping['group'])) {
1371 $claim .= $mapping['claim'];
1372 } else {
1373 $claim .= $mapping['group'];
1374 }
1375 if ($this->jwt->hasClaim($claim)) {
1376 $value = null;
1377 if (empty($mapping['group'])) {
1378 unset($payload->{$claim});
1379 $value = $this->jwt->getClaim($claim);
1380 } else {
1381 $group = $this->jwt->getClaim($claim);
1382 if (is_array($group) && array_key_exists($mapping['claim'], $group)) {
1383 unset($payload->{$claim}[$mapping['claim']]);
1384 $value = $group[$mapping['claim']];
1385 } elseif (is_object($group) && isset($group->{$mapping['claim']})) {
1386 unset($payload->{$claim}->{$mapping['claim']});
1387 $value = $group->{$mapping['claim']};
1388 }
1389 }
1390 if (!is_null($value)) {
1391 if (isset($mapping['isArray']) && $mapping['isArray']) {
1392 if (!is_array($value)) {
1393 $errors[] = "'{$claim}' claim must be an array";
1394 } else {
1395 $value = implode(',', $value);
1396 }
1397 } elseif (isset($mapping['isObject']) && $mapping['isObject']) {
1398 $value = json_encode($value);
1399 } elseif (isset($mapping['isBoolean']) && $mapping['isBoolean']) {
1400 $value = $value ? 'true' : 'false';
1401 } elseif (isset($mapping['isInteger']) && $mapping['isInteger']) {
1402 $value = strval($value);
1403 } elseif (!is_string($value)) {
1404 if ($generateWarnings) {
1405 $this->warnings[] = "Value of claim '{$claim}' is not a string: '{$value}'";
1406 }
1407 if (!$strictMode) {
1408 $value = strval($value);
1409 }
1410 }
1411 }
1412 if (!is_null($value) && is_string($value)) {
1413 $this->messageParameters[$key] = $value;
1414 }
1415 }
1416 }
1417 if (!empty($this->messageParameters['lti_message_type']) &&
1418 in_array($this->messageParameters['lti_message_type'], array_values(Util::MESSAGE_TYPE_MAPPING))) {
1419 $this->messageParameters['lti_message_type'] = array_search($this->messageParameters['lti_message_type'],
1421 }
1422 if (!empty($this->messageParameters['accept_types'])) {
1423 $types = array_map('trim', explode(',', $this->messageParameters['accept_types']));
1424 $types = array_filter($types);
1425 $mediaTypes = array();
1426 if (!empty($this->messageParameters['accept_media_types'])) {
1427 $mediaTypes = array_map('trim', explode(',', $this->messageParameters['accept_media_types']));
1428 $mediaTypes = array_filter($mediaTypes);
1429 }
1430 if (in_array(Item::TYPE_LTI_LINK, $types)) {
1431 $mediaTypes[] = Item::LTI_LINK_MEDIA_TYPE;
1432 }
1433 if (in_array(Item::TYPE_LTI_ASSIGNMENT, $types)) {
1434 $mediaTypes[] = Item::LTI_ASSIGNMENT_MEDIA_TYPE;
1435 }
1436 if (in_array('html', $types) && !in_array('*/*', $mediaTypes)) {
1437 $mediaTypes[] = 'text/html';
1438 }
1439 if (in_array('image', $types) && !in_array('*/*', $mediaTypes)) {
1440 $mediaTypes[] = 'image/*';
1441 }
1442 $mediaTypes = array_unique($mediaTypes);
1443 $this->messageParameters['accept_media_types'] = implode(',', $mediaTypes);
1444 }
1445 $claim = Util::JWT_CLAIM_PREFIX . '/claim/custom';
1446 if ($this->jwt->hasClaim($claim)) {
1447 unset($payload->{$claim});
1448 $custom = $this->jwt->getClaim($claim);
1449 if (!is_array($custom) && !is_object($custom)) {
1450 $errors[] = "'{$claim}' claim must be an object";
1451 } else {
1452 foreach ($custom as $key => $value) {
1453 $this->messageParameters["custom_{$key}"] = $value;
1454 }
1455 }
1456 }
1457 $claim = Util::JWT_CLAIM_PREFIX . '/claim/ext';
1458 if ($this->jwt->hasClaim($claim)) {
1459 unset($payload->{$claim});
1460 $ext = $this->jwt->getClaim($claim);
1461 if (!is_array($ext) && !is_object($ext)) {
1462 $errors[] = "'{$claim}' claim must be an object";
1463 } else {
1464 foreach ($ext as $key => $value) {
1465 $this->messageParameters["ext_{$key}"] = $value;
1466 }
1467 }
1468 }
1469 $claim = Util::JWT_CLAIM_PREFIX . '/claim/lti1p1';
1470 if ($this->jwt->hasClaim($claim)) {
1471 unset($payload->{$claim});
1472 $lti1p1 = $this->jwt->getClaim($claim);
1473 if (!is_array($lti1p1) && !is_object($lti1p1)) {
1474 $errors[] = "'{$claim}' claim must be an object";
1475 } else {
1476 foreach ($lti1p1 as $key => $value) {
1477 if (is_null($value)) {
1478 $value = '';
1479 } elseif (is_object($value)) {
1480 $value = json_encode($value);
1481 }
1482 $this->messageParameters["lti1p1_{$key}"] = $value;
1483 }
1484 }
1485 }
1486 $claim = 'http://www.brightspace.com';
1487 if ($this->jwt->hasClaim($claim)) {
1488 $d2l = $this->jwt->getClaim($claim);
1489 if (is_array($d2l)) {
1490 if (!empty($d2l['username'])) {
1491 $this->messageParameters['ext_d2l_username'] = $d2l['username'];
1492 unset($payload->{$claim}['username']);
1493 }
1494 } else if (is_object($ext)) {
1495 if (!empty($d2l->username)) {
1496 $this->messageParameters['ext_d2l_username'] = $d2l->username;
1497 unset($payload->{$claim}->username);
1498 }
1499 }
1500 }
1501 if (!empty($payload)) {
1502 $objVars = get_object_vars($payload);
1503 foreach ($objVars as $attrName => $attrValue) {
1504 if (empty((array) $attrValue)) {
1505 unset($payload->{$attrName});
1506 }
1507 }
1508 $this->messageParameters['unmapped_claims'] = json_encode($payload);
1509 }
1510 if (!empty($errors)) {
1511 $this->ok = false;
1512 $this->reason = 'Invalid JWT: ' . implode(', ', $errors);
1513 }
1514 }
1515
1521 private function doCallback()
1522 {
1523 if (array_key_exists($this->messageParameters['lti_message_type'], Util::$METHOD_NAMES)) {
1524 $callback = Util::$METHOD_NAMES[$this->messageParameters['lti_message_type']];
1525 } else {
1526 $callback = "on{$this->messageParameters['lti_message_type']}";
1527 }
1528 if (method_exists($this, $callback)) {
1529 $this->$callback();
1530 } elseif ($this->ok) {
1531 $this->ok = false;
1532 $this->reason = "Message type not supported: {$this->messageParameters['lti_message_type']}";
1533 }
1534 }
1535
1548 private function addOAuth1Signature($endpoint, $data, $method, $type, $hash, $timestamp)
1549 {
1550 $params = array();
1551 if (is_array($data)) {
1552 $params = $data;
1553 $params['oauth_callback'] = 'about:blank';
1554 }
1555// Check for query parameters which need to be included in the signature
1556 $queryString = parse_url($endpoint, PHP_URL_QUERY);
1557 $queryParams = OAuth\OAuthUtil::parse_parameters($queryString);
1558 $params = array_merge_recursive($queryParams, $params);
1559
1560 if (!is_array($data)) {
1561 if (empty($hash)) { // Calculate body hash
1562 if (is_null($data)) {
1563 $data = '';
1564 }
1565 switch ($this->signatureMethod) {
1566 case 'HMAC-SHA224':
1567 $hash = base64_encode(hash('sha224', $data, true));
1568 break;
1569 case 'HMAC-SHA256':
1570 $hash = base64_encode(hash('sha256', $data, true));
1571 break;
1572 case 'HMAC-SHA384':
1573 $hash = base64_encode(hash('sha384', $data, true));
1574 break;
1575 case 'HMAC-SHA512':
1576 $hash = base64_encode(hash('sha512', $data, true));
1577 break;
1578 default:
1579 $hash = base64_encode(sha1($data, true));
1580 break;
1581 }
1582 }
1583 $params['oauth_body_hash'] = $hash;
1584 }
1585 if (!empty($timestamp)) {
1586 $params['oauth_timestamp'] = $timestamp;
1587 }
1588
1589// Add OAuth signature
1590 switch ($this->signatureMethod) {
1591 case 'HMAC-SHA224':
1592 $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA224();
1593 break;
1594 case 'HMAC-SHA256':
1595 $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA256();
1596 break;
1597 case 'HMAC-SHA384':
1598 $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA384();
1599 break;
1600 case 'HMAC-SHA512':
1601 $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA512();
1602 break;
1603 default:
1604 $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA1();
1605 break;
1606 }
1607 $key = $this->key;
1608 $secret = $this->secret;
1609 if (empty($key)) {
1610 if (($this instanceof Tool) && !empty($this->platform)) {
1611 $key = $this->platform->getKey();
1612 $secret = $this->platform->secret;
1613 } elseif (($this instanceof Platform) && !empty(Tool::$defaultTool)) {
1614 $key = Tool::$defaultTool->getKey();
1615 $secret = Tool::$defaultTool->secret;
1616 }
1617 }
1618 $oauthConsumer = new OAuth\OAuthConsumer($key, $secret, null);
1619 $oauthReq = OAuth\OAuthRequest::from_consumer_and_token($oauthConsumer, null, $method, $endpoint, $params);
1620 $oauthReq->sign_request($hmacMethod, $oauthConsumer, null);
1621 if (!is_array($data)) {
1622 $header = $oauthReq->to_header();
1623 if (empty($data)) {
1624 if (!empty($type)) {
1625 $header .= "\nAccept: {$type}";
1626 }
1627 } elseif (isset($type)) {
1628 $header .= "\nContent-Type: {$type}; charset=UTF-8";
1629 $header .= "\nContent-Length: " . strlen($data);
1630 }
1631 return $header;
1632 } else {
1633// Remove parameters from query string
1634 $params = $oauthReq->get_parameters();
1635 foreach ($queryParams as $key => $value) {
1636 if (!is_array($value)) {
1637 if (!is_array($params[$key])) {
1638 if ($params[$key] === $value) {
1639 unset($params[$key]);
1640 }
1641 } else {
1642 $params[$key] = array_diff($params[$key], array($value));
1643 }
1644 } else {
1645 foreach ($value as $element) {
1646 $params[$key] = array_diff($params[$key], array($value));
1647 }
1648 }
1649 }
1650// Remove any parameters comprising an empty array of values
1651 foreach ($params as $key => $value) {
1652 if (is_array($value)) {
1653 if (count($value) <= 0) {
1654 unset($params[$key]);
1655 } elseif (count($value) === 1) {
1656 $params[$key] = reset($value);
1657 }
1658 }
1659 }
1660 return $params;
1661 }
1662 }
1663
1676 private function addJWTSignature($endpoint, $data, $method, $type, $nonce, $timestamp)
1677 {
1678 $ok = false;
1679 if (is_array($data)) {
1680 $ok = true;
1681 if (empty($nonce)) {
1682 $nonce = Util::getRandomString(32);
1683 }
1684 $publicKey = null;
1685 if (!array_key_exists('grant_type', $data)) {
1686 $this->messageParameters = $data;
1687 $payload = $this->getMessageClaims();
1688 $privateKey = $this->rsaKey;
1689 $kid = $this->kid;
1690 $jku = $this->jku;
1691 if ($this instanceof Platform) {
1692 if (!empty(Tool::$defaultTool)) {
1693 $publicKey = Tool::$defaultTool->rsaKey;
1694 }
1695 $payload['iss'] = $this->platformId;
1696 $payload['aud'] = array($this->clientId);
1697 $payload['azp'] = $this->clientId;
1698 $payload[Util::JWT_CLAIM_PREFIX . '/claim/deployment_id'] = $this->deploymentId;
1699 $payload[Util::JWT_CLAIM_PREFIX . '/claim/target_link_uri'] = $endpoint;
1700 $paramName = 'id_token';
1701 } else {
1702 if (!empty($this->platform)) {
1703 $publicKey = $this->platform->rsaKey;
1704 $payload['iss'] = $this->platform->clientId;
1705 $payload['aud'] = array($this->platform->platformId);
1706 $payload['azp'] = $this->platform->platformId;
1707 $payload[Util::JWT_CLAIM_PREFIX . '/claim/deployment_id'] = $this->platform->deploymentId;
1708 }
1709 $paramName = 'JWT';
1710 }
1711 $payload['nonce'] = $nonce;
1712 } else {
1713 $authorizationId = '';
1714 if ($this instanceof Tool) {
1715 $sub = '';
1716 if (!empty($this->platform)) {
1717 $sub = $this->platform->clientId;
1718 $authorizationId = $this->platform->authorizationServerId;
1719 $publicKey = $this->platform->rsaKey;
1720 }
1721 $privateKey = $this->rsaKey;
1722 $kid = $this->kid;
1723 $jku = $this->jku;
1724 } else { // Tool-hosted services not yet defined in LTI
1725 $sub = $this->clientId;
1726 $kid = $this->kid;
1727 $jku = $this->jku;
1728 $privateKey = $this->rsaKey;
1729 if (!empty(Tool::$defaultTool)) {
1730 $publicKey = Tool::$defaultTool->rsaKey;
1731 }
1732 }
1733 $payload['iss'] = $sub;
1734 $payload['sub'] = $sub;
1735 if (empty($authorizationId)) {
1736 $authorizationId = $endpoint;
1737 }
1738 $payload['aud'] = array($authorizationId);
1739 $payload['jti'] = $nonce;
1740 $params = $data;
1741 $paramName = 'client_assertion';
1742 }
1743 }
1744 if ($ok) {
1745 if (empty($timestamp)) {
1746 $timestamp = time();
1747 }
1748 $payload['iat'] = $timestamp;
1749 $payload['exp'] = $timestamp + Jwt::$life;
1750 try {
1751 $jwt = Jwt::getJwtClient();
1752 $params[$paramName] = $jwt::sign($payload, $this->signatureMethod, $privateKey, $kid, $jku, $this->encryptionMethod,
1753 $publicKey);
1754 } catch (\Exception $e) {
1755 $params = array();
1756 }
1757
1758 return $params;
1759 } else {
1760 $header = '';
1761 if ($this instanceof Tool) {
1762 $platform = $this->platform;
1763 } else {
1764 $platform = $this;
1765 }
1766 $accessToken = $platform->getAccessToken();
1767 if (empty($accessToken)) {
1768 $accessToken = new AccessToken($platform);
1769 $platform->setAccessToken($accessToken);
1770 }
1771 if (!$accessToken->hasScope()) { // Check token has not expired
1772 $accessToken->get();
1773 }
1774 if (!empty($accessToken->token)) {
1775 $header = "Authorization: Bearer {$accessToken->token}";
1776 }
1777 if (empty($data) && ($method !== 'DELETE')) {
1778 if (!empty($type)) {
1779 $header .= "\nAccept: {$type}";
1780 }
1781 } elseif (isset($type)) {
1782 $header .= "\nContent-Type: {$type}; charset=UTF-8";
1783 if (!empty($data) && is_string($data)) {
1784 $header .= "\nContent-Length: " . strlen($data);
1785 }
1786 }
1787
1788 return $header;
1789 }
1790 }
1791
1800 private static function fullyQualifyClaim($claim, $value)
1801 {
1802 $claims = array();
1803 $empty = true;
1804 if (is_object($value)) {
1805 foreach ($value as $c => $v) {
1806 $empty = false;
1807 $claims = array_merge($claims, static::fullyQualifyClaim("{$claim}/{$c}", $v));
1808 }
1809 }
1810 if ($empty) {
1811 $claims[$claim] = $value;
1812 }
1813
1814 return $claims;
1815 }
1816
1817}
Class to represent a content-item object.
Class to provide a connection to a persistent store for LTI objects.
Class to represent an HTTP message request.
Class to represent an HTTP message request.
Definition Jwt.php:15
Class to represent an OAuth Consumer.
Class to represent an OAuth Data Store.
static $POST_INPUT
Access to POST data.
Class to represent an OAuth server.
Class to represent an OAuth HMAC_SHA1 signature method.
Class to represent an OAuth HMAC_SHA224 signature method.
Class to represent an OAuth HMAC_SHA256 signature method.
Class to represent an OAuth HMAC_SHA384 signature method.
Class to represent an OAuth HMAC_SHA512 signature method.
Class to represent a platform nonce.
Class to represent a platform.
Definition Platform.php:18
static fromPlatformId($platformId, $clientId, $deploymentId, $dataConnector=null, $autoEnable=false)
Load the platform from the database by its platform, client and deployment IDs.
Definition Platform.php:511
static fromConsumerKey($key=null, $dataConnector=null, $autoEnable=false)
Load the platform from the database by its consumer key.
Definition Platform.php:486
Class to represent an LTI system.
Definition System.php:22
sendMessage($url, $type, $messageParams, $target='', $userId=null, $hint=null)
Generate a web page containing an auto-submitted form of LTI message parameters.
Definition System.php:948
$warnings
Warnings relating to last request processed.
Definition System.php:123
addSignature($endpoint, $data, $method='POST', $type=null, $nonce='', $hash=null, $timestamp=null)
Add the signature to an array of message parameters or to a header string.
Definition System.php:1024
$reason
Error message for last request processed.
Definition System.php:109
getRawParameters()
Get the raw POST parameters.
Definition System.php:362
$debugMode
Whether debug level messages are to be reported.
Definition System.php:130
$name
Local name of platform/tool.
Definition System.php:43
$enableUntil
Timestamp until which the system instance is enabled to accept connection requests.
Definition System.php:151
getMessageClaims($fullyQualified=false)
Get the message claims.
Definition System.php:378
$enabled
Whether the system instance is enabled to accept connection requests.
Definition System.php:137
$secret
Shared secret.
Definition System.php:50
hasJwt()
Check whether a JWT exists.
Definition System.php:342
signMessage(&$url, $type, $version, $params, $loginHint=null, $ltiMessageHint=null)
Add the signature to an LTI message.
Definition System.php:889
$dataConnector
Data connector object.
Definition System.php:71
verifySignature()
Verify the signature of a message.
Definition System.php:1095
$messageParameters
LTI message parameters.
Definition System.php:193
$updated
Timestamp for when the object was last updated.
Definition System.php:172
setSetting($name, $value=null)
Set a setting value.
Definition System.php:288
signParameters($url, $type, $version, $params)
Add the signature to an LTI message.
Definition System.php:861
$rsaKey
RSA key in PEM or JSON format.
Definition System.php:81
$encryptionMethod
Algorithm used for encrypting messages.
Definition System.php:64
$details
Details for error message relating to last request processed.
Definition System.php:116
$jku
Endpoint for public key.
Definition System.php:102
saveSettings()
Save setting values.
Definition System.php:326
$created
Timestamp for when the object was created.
Definition System.php:165
$ok
True if the last request was successful.
Definition System.php:29
$rawParameters
Raw message parameters.
Definition System.php:186
setKey($key)
Set the consumer key.
Definition System.php:258
$enableFrom
Timestamp from which the the system instance is enabled to accept connection requests.
Definition System.php:144
$ltiVersion
LTI version.
Definition System.php:36
setSettings($settings)
Set an array of all setting values.
Definition System.php:316
signServiceRequest($url, $method, $type, $data=null)
Generates the headers for an LTI service request.
Definition System.php:966
$lastAccess
Timestamp for date of last connection to this system.
Definition System.php:158
useOAuth1()
Determine whether this consumer is using the OAuth 1 security model.
Definition System.php:1006
checkMessage()
Verify the required properties of an LTI message.
Definition System.php:1038
getRecordId()
Get the system record ID.
Definition System.php:228
getKey()
Get the consumer key.
Definition System.php:248
getSetting($name, $default='')
Get a setting value.
Definition System.php:271
setRecordId($id)
Sets the system record ID.
Definition System.php:238
$requiredScopes
Scopes to request when obtaining an access token.
Definition System.php:88
doServiceRequest($service, $method, $format, $data)
Perform a service request.
Definition System.php:986
getSettings()
Get an array of all setting values.
Definition System.php:306
getJwt()
Get the JWT.
Definition System.php:352
static parseRoles($roles, $ltiVersion=Util::LTI_VERSION1, $addPrincipalRole=false)
Parse a set of roles to comply with a specified version of LTI.
Definition System.php:568
$jwt
JWT ClientInterface object.
Definition System.php:179
$signatureMethod
Method used for signing messages.
Definition System.php:57
Class to represent an LTI Tool.
Definition Tool.php:24
static $defaultTool
Default tool for use with service requests.
Definition Tool.php:294
Class to implement utility methods.
Definition Util.php:15
static sendForm($url, $params, $target='', $javascript='')
Generate a web page containing an auto-submitted form of parameters.
Definition Util.php:378
const LTI_VERSION1P3
LTI version 1.3 for messages.
Definition Util.php:25
const JWT_CLAIM_MAPPING
Mapping for standard message parameters to JWT claim.
Definition Util.php:50
const MESSAGE_TYPE_MAPPING
Mapping for standard message types.
Definition Util.php:40
static getRandomString($length=8)
Generate a random string.
Definition Util.php:523
static cloneObject($obj)
Clone an object and any objects it contains.
Definition Util.php:578
static $METHOD_NAMES
List of supported message types and associated class methods.
Definition Util.php:176
static $LTI_VERSIONS
Permitted LTI versions for messages.
Definition Util.php:169
const LTI_VERSION1
LTI version 1 for messages.
Definition Util.php:20
static jsonDecode($str, $associative=false)
Decode a JSON string.
Definition Util.php:560
static setTestCookie($delete=false)
Set or delete a test cookie.
Definition Util.php:488
const JWT_CLAIM_PREFIX
Prefix for standard JWT message claims.
Definition Util.php:35
const LTI_VERSION2
LTI version 2 for messages.
Definition Util.php:30
Interface to represent an HWT client.