LTI Integration Library 4.10.3
PHP class library for building LTI integrations
 
Loading...
Searching...
No Matches
ResourceLink.php
1<?php
2
3namespace ceLTIc\LTI;
4
9use DOMDocument;
10
19{
20 use ApiHook;
21
25 const EXT_READ = 1;
26
30 const EXT_WRITE = 2;
31
35 const EXT_DELETE = 3;
36
40 const EXT_CREATE = 4;
41
45 const EXT_UPDATE = 5;
46
50 const EXT_TYPE_DECIMAL = 'decimal';
51
55 const EXT_TYPE_PERCENTAGE = 'percentage';
56
60 const EXT_TYPE_RATIO = 'ratio';
61
65 const EXT_TYPE_LETTER_AF = 'letteraf';
66
70 const EXT_TYPE_LETTER_AF_PLUS = 'letterafplus';
71
75 const EXT_TYPE_PASS_FAIL = 'passfail';
76
80 const EXT_TYPE_TEXT = 'freetext';
81
87 public $title = null;
88
94 public $ltiResourceLinkId = null;
95
109 public $groupSets = null;
110
121 public $groups = null;
122
128 public $lastServiceRequest = null;
129
135 public $extRequest = null;
136
142 public $extRequestHeaders = null;
143
149 public $extResponse = null;
150
156 public $extResponseHeaders = null;
157
163 public $primaryResourceLinkId = null;
164
170 public $shareApproved = null;
171
177 public $created = null;
178
184 public $updated = null;
185
191 private $id = null;
192
198 private $platform = null;
199
205 private $platformId = null;
206
212 private $context = null;
213
219 private $contextId = null;
220
226 private $settings = null;
227
233 private $settingsChanged = false;
234
240 private $extDoc = null;
241
247 private $extNodes = null;
248
254 private $dataConnector = null;
255
259 public function __construct()
260 {
261 $this->initialize();
262 }
263
267 public function initialize()
268 {
269 $this->title = '';
270 $this->settings = array();
271 $this->groupSets = null;
272 $this->groups = null;
273 $this->primaryResourceLinkId = null;
274 $this->shareApproved = null;
275 $this->created = null;
276 $this->updated = null;
277 }
278
284 public function initialise()
285 {
286 $this->initialize();
287 }
288
294 public function save()
295 {
296 $ok = $this->getDataConnector()->saveResourceLink($this);
297 if ($ok) {
298 $this->settingsChanged = false;
299 }
300
301 return $ok;
302 }
303
309 public function delete()
310 {
311 return $this->getDataConnector()->deleteResourceLink($this);
312 }
313
322 public function getConsumer()
323 {
324 Util::logDebug('Method ceLTIc\LTI\ResourceLink::getConsumer() has been deprecated; please use ceLTIc\LTI\ResourceLink::getPlatform() instead.',
325 true);
326 return $this->getPlatform();
327 }
328
337 public function getConsumerId()
338 {
339 Util::logDebug('Method ceLTIc\LTI\ResourceLink::getConsumerId() has been deprecated; please use ceLTIc\LTI\ResourceLink::getPlatformId() instead.',
340 true);
341 return $this->getPlatformId();
342 }
343
352 public function setConsumerId($consumerId)
353 {
354 Util::logDebug('Method ceLTIc\LTI\ResourceLink::setConsumerId() has been deprecated; please use ceLTIc\LTI\ResourceLink::setPlatformId() instead.',
355 true);
356 $this->setPlatformId($consumerId);
357 }
358
364 public function getPlatform()
365 {
366 if (is_null($this->platform)) {
367 if (!is_null($this->context) || !is_null($this->contextId)) {
368 $this->platform = $this->getContext()->getPlatform();
369 } else {
370 $this->platform = Platform::fromRecordId($this->platformId, $this->getDataConnector());
371 }
372 }
373
374 return $this->platform;
375 }
376
382 public function getPlatformId()
383 {
384 return $this->platformId;
385 }
386
392 public function setPlatformId($platformId)
393 {
394 $this->platform = null;
395 $this->platformId = $platformId;
396 }
397
403 public function getContext()
404 {
405 if (is_null($this->context) && !is_null($this->contextId)) {
406 $this->context = Context::fromRecordId($this->contextId, $this->getDataConnector());
407 }
408
409 return $this->context;
410 }
411
417 public function getContextId()
418 {
419 if (is_null($this->contextId) && !is_null($this->context)) {
420 $this->contextId = $this->context->getRecordId();
421 }
422
423 return $this->contextId;
424 }
425
431 public function setContext($context)
432 {
433 $this->context = $context;
434 $this->contextId = $context->getRecordId();
435 }
436
442 public function setContextId($contextId)
443 {
444 if ($this->contextId !== $contextId) {
445 $this->context = null;
446 $this->contextId = $contextId;
447 }
448 }
449
455 public function getKey()
456 {
457 return $this->getPlatform()->getKey();
458 }
459
465 public function getId()
466 {
468 }
469
475 public function getRecordId()
476 {
477 return $this->id;
478 }
479
485 public function setRecordId($id)
486 {
487 $this->id = $id;
488 }
489
495 public function getDataConnector()
496 {
497 if (empty($this->dataConnector)) {
498 $this->getPlatform();
499 if (!empty($this->platform)) {
500 $this->dataConnector = $this->platform->getDataConnector();
501 }
502 }
503
504 return $this->dataConnector;
505 }
506
515 public function getSetting($name, $default = '')
516 {
517 if (array_key_exists($name, $this->settings)) {
518 $value = $this->settings[$name];
519 } else {
520 $value = $default;
521 }
522
523 return $value;
524 }
525
532 public function setSetting($name, $value = null)
533 {
534 $old_value = $this->getSetting($name);
535 if ($value !== $old_value) {
536 if (!empty($value)) {
537 $this->settings[$name] = $value;
538 } else {
539 unset($this->settings[$name]);
540 }
541 $this->settingsChanged = true;
542 }
543 }
544
550 public function getSettings()
551 {
552 return $this->settings;
553 }
554
560 public function setSettings($settings)
561 {
562 $this->settings = $settings;
563 }
564
570 public function saveSettings()
571 {
572 if ($this->settingsChanged) {
573 $ok = $this->save();
574 } else {
575 $ok = true;
576 }
577
578 return $ok;
579 }
580
586 public function hasOutcomesService()
587 {
588 $has = !empty($this->getSetting('ext_ims_lis_basic_outcome_url')) || !empty($this->getSetting('lis_outcome_service_url'));
589 if (!$has && !empty($this->getSetting('custom_lineitem_url')) && !empty($this->getSetting('custom_ags_scopes'))) {
590 $scopes = explode(',', $this->getSetting('custom_ags_scopes'));
591 $has = in_array(Service\Score::$SCOPE, $scopes) && in_array(Service\Result::$SCOPE, $scopes);
592 }
593 if (!$has) {
594 $has = self::hasConfiguredApiHook(self::$OUTCOMES_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this);
595 }
596 return $has;
597 }
598
604 public function hasMembershipsService()
605 {
606 $has = false;
607 if (!empty($this->getContextId())) {
608 $has = !empty($this->getContext()->getSetting('custom_context_memberships_url')) || !empty($this->getContext()->getSetting('custom_context_memberships_v2_url'));
609 }
610 if (!$has) {
611 $has = !empty($this->getSetting('custom_link_memberships_url'));
612 }
613 if (!$has) {
614 $has = !empty($this->getSetting('ext_ims_lis_memberships_url'));
615 }
616 if (!$has) {
617 $has = self::hasConfiguredApiHook(self::$MEMBERSHIPS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this);
618 }
619
620 return $has;
621 }
622
628 public function hasSettingService()
629 {
630 $url = $this->getSetting('ext_ims_lti_tool_setting_url');
631
632 return !empty($url);
633 }
634
640 public function hasLineItemService()
641 {
642 $has = false;
643 if (!empty($this->getSetting('custom_ags_scopes'))) {
644 $scopes = explode(',', $this->getSetting('custom_ags_scopes'));
645 if (in_array(Service\LineItem::$SCOPE, $scopes) || in_array(Service\LineItem::$SCOPE_READONLY, $scopes)) {
646 $has = !empty($this->getSetting('custom_lineitems_url'));
647 }
648 }
649
650 return $has;
651 }
652
658 public function hasScoreService()
659 {
660 $has = false;
661 if (!empty($this->getSetting('custom_ags_scopes'))) {
662 $scopes = explode(',', $this->getSetting('custom_ags_scopes'));
663 if (in_array(Service\Score::$SCOPE, $scopes)) {
664 $has = !empty($this->getSetting('custom_lineitem_url'));
665 }
666 }
667
668 return $has;
669 }
670
676 public function hasResultService()
677 {
678 $has = false;
679 if (!empty($this->getSetting('custom_ags_scopes'))) {
680 $scopes = explode(',', $this->getSetting('custom_ags_scopes'));
681 if (in_array(Service\Result::$SCOPE, $scopes)) {
682 $has = !empty($this->getSetting('custom_lineitem_url'));
683 }
684 }
685
686 return $has;
687 }
688
694 public function hasAssessmentControlService()
695 {
696 $url = $this->getSetting('custom_ap_acs_url');
697
698 return !empty($url);
699 }
700
710 public function doOutcomesService($action, $ltiOutcome, $userResult)
711 {
712 $ok = false;
713 $this->extResponse = '';
714// Lookup service details from the source resource link appropriate to the user (in case the destination is being shared)
715 $sourceResourceLink = $userResult->getResourceLink();
716 $sourcedId = $userResult->ltiResultSourcedId;
717
718// Use LTI 1.1 service in preference to extension service if it is available
719 $urlAGS = $sourceResourceLink->getSetting('custom_lineitem_url');
720 $urlLTI11 = $sourceResourceLink->getSetting('lis_outcome_service_url');
721 $urlExt = $sourceResourceLink->getSetting('ext_ims_lis_basic_outcome_url');
722
723 if (!empty($urlAGS)) {
724 if (($action === self::EXT_READ) && ($ltiOutcome->type === self::EXT_TYPE_DECIMAL) && $sourceResourceLink->hasResultService()) {
725 $ltiOutcome = $this->doResultService($userResult, $urlAGS);
726 $ok = !empty($ltiOutcome);
727 } elseif ((($action === self::EXT_WRITE) && $this->checkValueType($ltiOutcome, array(self::EXT_TYPE_DECIMAL)) && $sourceResourceLink->hasScoreService()) ||
728 ($action === self::EXT_DELETE)) {
729 if ($action === self::EXT_DELETE) {
730 $ltiOutcome->setValue(null);
731 $ltiOutcome->activityProgress = 'Initialized';
732 $ltiOutcome->gradingProgress = 'NotReady';
733 }
734 $ok = $this->doScoreService($ltiOutcome, $userResult, $urlAGS);
735 }
736 }
737 if (!$ok && is_null($ltiOutcome->getValue())) {
738 $ltiOutcome->setValue('');
739 }
740 if (!$ok && !empty($urlLTI11)) {
741 $do = '';
742 $outcome = $ltiOutcome->getValue();
743 if (($action === self::EXT_READ) && ($ltiOutcome->type === self::EXT_TYPE_DECIMAL)) {
744 $do = 'readResult';
745 } elseif (($action === self::EXT_WRITE) && $this->checkValueType($ltiOutcome, array(self::EXT_TYPE_DECIMAL))) {
746 $do = 'replaceResult';
747 if (($ltiOutcome->getPointsPossible() <> 1) && ($ltiOutcome->getPointsPossible() > 0)) {
748 $outcome = $outcome / $ltiOutcome->getPointsPossible();
749 }
750 } elseif ($action === self::EXT_DELETE) {
751 $do = 'deleteResult';
752 }
753 if (!empty($do)) {
754 $xml = '';
755 if ($action === self::EXT_WRITE) {
756 $comment = (empty($ltiOutcome->comment)) ? '' : trim($ltiOutcome->comment);
757 if (!empty($comment) && !empty($sourceResourceLink->getSetting('ext_outcome_data_values_accepted'))) {
758 $resultDataTypes = explode(',', $sourceResourceLink->getSetting('ext_outcome_data_values_accepted'));
759 $resultDataType = '';
760 if (count($resultDataTypes) === 1) {
761 $resultDataType = $resultDataTypes[0];
762 } elseif (count($resultDataTypes) > 1) {
763 $isUrl = (strpos($comment, 'http://') === 0) || (strpos($comment, 'https://') === 0);
764 if ($isUrl && in_array('ltiLaunchUrl', $resultDataTypes)) {
765 $resultDataType = 'ltiLaunchUrl';
766 } elseif ($isUrl && in_array('url', $resultDataTypes)) {
767 $resultDataType = 'url';
768 } elseif (in_array('text', $resultDataTypes)) {
769 $resultDataType = 'text';
770 }
771 }
772 if (!empty($resultDataType)) {
773 $xml = <<< EOF
774
775 <resultData>
776 <{$resultDataType}>{$comment}</{$resultDataType}>
777 </resultData>
778EOF;
779 }
780 }
781 $xml = <<< EOF
782
783 <result>
784 <resultScore>
785 <language>{$ltiOutcome->language}</language>
786 <textString>{$outcome}</textString>
787 </resultScore>{$xml}
788 </result>
789EOF;
790 }
791 $sourcedId = htmlentities($sourcedId);
792 $xml = <<<EOF
793 <resultRecord>
794 <sourcedGUID>
795 <sourcedId>{$sourcedId}</sourcedId>
796 </sourcedGUID>{$xml}
797 </resultRecord>
798EOF;
799 if ($this->doLTI11Service($do, $urlLTI11, $xml)) {
800 switch ($action) {
801 case self::EXT_READ:
802 if (!isset($this->extNodes['imsx_POXBody']["{$do}Response"]['result']['resultScore']['textString'])) {
803 break;
804 } else {
805 $ltiOutcome->setValue($this->extNodes['imsx_POXBody']["{$do}Response"]['result']['resultScore']['textString']);
806 }
807 case self::EXT_WRITE:
808 case self::EXT_DELETE:
809 $ok = true;
810 break;
811 }
812 }
813 }
814 }
815 if (!$ok && !empty($urlExt)) {
816 $do = '';
817 $outcome = $ltiOutcome->getValue();
818 if (($action === self::EXT_READ) && ($ltiOutcome->type === self::EXT_TYPE_DECIMAL)) {
819 $do = 'basic-lis-readresult';
820 } elseif (($action === self::EXT_WRITE) && $this->checkValueType($ltiOutcome, array(self::EXT_TYPE_DECIMAL))) {
821 $do = 'basic-lis-updateresult';
822 if (($ltiOutcome->getPointsPossible() <> 1) && ($ltiOutcome->getPointsPossible() > 0)) {
823 $outcome = $outcome / $ltiOutcome->getPointsPossible();
824 }
825 } elseif ($action === self::EXT_DELETE) {
826 $do = 'basic-lis-deleteresult';
827 }
828 if (!empty($do)) {
829 $params = array();
830 $params['sourcedid'] = $sourcedId;
831 $params['result_resultscore_textstring'] = $outcome;
832 if (!empty($ltiOutcome->language)) {
833 $params['result_resultscore_language'] = $ltiOutcome->language;
834 }
835 if (!empty($ltiOutcome->status)) {
836 $params['result_statusofresult'] = $ltiOutcome->status;
837 }
838 if (!empty($ltiOutcome->date)) {
839 $params['result_date'] = $ltiOutcome->date;
840 }
841 if (!empty($ltiOutcome->type)) {
842 $params['result_resultvaluesourcedid'] = $ltiOutcome->type;
843 }
844 if (!empty($ltiOutcome->dataSource)) {
845 $params['result_datasource'] = $ltiOutcome->dataSource;
846 }
847 if ($this->doService($do, $urlExt, $params, 'https://purl.imsglobal.org/spec/lti-ext/scope/outcomes')) {
848 switch ($action) {
849 case self::EXT_READ:
850 if (isset($this->extNodes['result']['resultscore']['textstring'])) {
851 $ltiOutcome->setValue($this->extNodes['result']['resultscore']['textstring']);
852 }
853 case self::EXT_WRITE:
854 case self::EXT_DELETE:
855 $ok = true;
856 break;
857 }
858 }
859 }
860 }
861 if ((!$ok) && $this->hasConfiguredApiHook(self::$OUTCOMES_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this)) {
862 $className = $this->getApiHook(self::$OUTCOMES_SERVICE_HOOK, $this->getPlatform()->getFamilyCode());
863 $hook = new $className($this);
864 $response = $hook->doOutcomesService($action, $ltiOutcome, $userResult);
865 if ($response !== false) {
866 $ok = true;
867 if ($action === self::EXT_READ) {
868 $ltiOutcome->setValue($response);
869 }
870 }
871 }
872
873 return $ok;
874 }
875
888 public function doMembershipsService($withGroups = false)
889 {
890 Util::logDebug('Method ceLTIc\LTI\ResourceLink::doMembershipsService() has been deprecated; please use ceLTIc\LTI\ResourceLink::getMemberships() instead.',
891 true);
892 return $this->getMemberships($withGroups);
893 }
894
903 public function doSettingService($action, $value = null)
904 {
905 $response = false;
906 $this->extResponse = '';
907 switch ($action) {
908 case self::EXT_READ:
909 $do = 'basic-lti-loadsetting';
910 break;
911 case self::EXT_WRITE:
912 $do = 'basic-lti-savesetting';
913 break;
914 case self::EXT_DELETE:
915 $do = 'basic-lti-deletesetting';
916 break;
917 }
918 if (isset($do)) {
919
920 $url = $this->getSetting('ext_ims_lti_tool_setting_url');
921 $params = array();
922 $params['id'] = $this->getSetting('ext_ims_lti_tool_setting_id');
923 if (is_null($value)) {
924 $value = '';
925 }
926 $params['setting'] = $value;
927
928 if ($this->doService($do, $url, $params, 'https://purl.imsglobal.org/spec/lti-ext/scope/setting')) {
929 switch ($action) {
930 case self::EXT_READ:
931 if (isset($this->extNodes['setting']['value'])) {
932 $response = $this->extNodes['setting']['value'];
933 if (is_array($response)) {
934 $response = '';
935 }
936 }
937 break;
938 case self::EXT_WRITE:
939 $this->setSetting('ext_ims_lti_tool_setting', $value);
940 $this->saveSettings();
941 $response = true;
942 break;
943 case self::EXT_DELETE:
944 $response = true;
945 break;
946 }
947 }
948 }
949
950 return $response;
951 }
952
958 public function hasToolSettingsService()
959 {
960 $has = !empty($this->getSetting('custom_link_setting_url'));
961 if (!$has) {
962 $has = self::hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this);
963 }
964 return $has;
965 }
966
975 public function getToolSettings($mode = Service\ToolSettings::MODE_CURRENT_LEVEL, $simple = true)
976 {
977 $ok = false;
978 $settings = array();
979 if (!empty($this->getSetting('custom_link_setting_url'))) {
980 $url = $this->getSetting('custom_link_setting_url');
981 $service = new Service\ToolSettings($this, $url, $simple);
982 $settings = $service->get($mode);
983 $this->lastServiceRequest = $service->getHttpMessage();
984 $ok = $settings !== false;
985 }
986 if (!$ok && $this->hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this)) {
987 $className = $this->getApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode());
988 $hook = new $className($this);
989 $settings = $hook->getToolSettings($mode, $simple);
990 }
991
992 return $settings;
993 }
994
1002 public function setToolSettings($settings = array())
1003 {
1004 $ok = false;
1005 if (!empty($this->getSetting('custom_link_setting_url'))) {
1006 $url = $this->getSetting('custom_link_setting_url');
1007 $service = new Service\ToolSettings($this, $url);
1008 $ok = $service->set($settings);
1009 $this->lastServiceRequest = $service->getHttpMessage();
1010 }
1011 if (!$ok && $this->hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this)) {
1012 $className = $this->getApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode());
1013 $hook = new $className($this);
1014 $ok = $hook->setToolSettings($settings);
1015 }
1016
1017 return $ok;
1018 }
1019
1028 public function hasMembershipService()
1029 {
1030 Util::logDebug('Method ceLTIc\LTI\ResourceLink::hasMembershipService() has been deprecated; please use ceLTIc\LTI\ResourceLink::hasMembershipsService() instead.',
1031 true);
1032 return $this->hasMembershipsService();
1033 }
1034
1043 public function getMembership()
1044 {
1045 Util::logDebug('Method ceLTIc\LTI\ResourceLink::getMembership() has been deprecated; please use ceLTIc\LTI\ResourceLink::getMemberships() instead.',
1046 true);
1047 return $this->getMemberships();
1048 }
1049
1057 public function getMemberships($withGroups = false)
1058 {
1059 $ok = false;
1060 $userResults = array();
1061 $hasLtiLinkService = !empty($this->getSetting('custom_link_memberships_url'));
1062 $hasLtiContextService = !empty($this->getContextId()) &&
1063 (!empty($this->getContext()->getSetting('custom_context_memberships_url')) || !empty($this->getContext()->getSetting('custom_context_memberships_v2_url')));
1064 $hasGroupsService = !empty($this->getContextId()) && !empty($this->getContext()->getSetting('custom_context_groups_url'));
1065 $hasExtService = !empty($this->getSetting('ext_ims_lis_memberships_url'));
1066 $hasApiHook = $this->hasConfiguredApiHook(self::$MEMBERSHIPS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this);
1067 if (($hasLtiContextService && (!$withGroups || $hasGroupsService)) || (!$hasExtService && !$hasApiHook)) {
1068 if (!empty($this->getContextId()) && !empty($this->getContext()->getSetting('custom_context_memberships_v2_url'))) {
1069 $url = $this->getContext()->getSetting('custom_context_memberships_v2_url');
1070 $format = Service\Membership::MEDIA_TYPE_MEMBERSHIPS_NRPS;
1071 } else {
1072 $url = $this->getContext()->getSetting('custom_context_memberships_url');
1073 $format = Service\Membership::MEDIA_TYPE_MEMBERSHIPS_V1;
1074 }
1075 $service = new Service\Membership($this, $url, $format);
1076 if (!$withGroups) {
1077 $userResults = $service->get();
1078 } else {
1079 $userResults = $service->getWithGroups();
1080 }
1081 $this->lastServiceRequest = $service->getHttpMessage();
1082 $ok = $userResults !== false;
1083 } elseif ($hasLtiLinkService) {
1084 $id = $this->id;
1085 $this->id = null;
1086 $url = $this->getSetting('custom_link_memberships_url');
1087 $format = Service\Membership::MEDIA_TYPE_MEMBERSHIPS_V1;
1088 $service = new Service\Membership($this, $url, $format);
1089 if (!$withGroups) {
1090 $userResults = $service->get();
1091 } else {
1092 $userResults = $service->getWithGroups();
1093 }
1094 $this->lastServiceRequest = $service->getHttpMessage();
1095 $this->id = $id;
1096 $ok = $userResults !== false;
1097 }
1098 if (!$ok && $hasExtService) {
1099 $this->extResponse = '';
1100 $url = $this->getSetting('ext_ims_lis_memberships_url');
1101 $params = array();
1102 $params['id'] = $this->getSetting('ext_ims_lis_memberships_id');
1103 if ($withGroups) {
1104 $ok = $this->doService('basic-lis-readmembershipsforcontextwithgroups', $url, $params,
1105 'https://purl.imsglobal.org/spec/lti-ext/scope/memberships');
1106 }
1107 if (!$ok) {
1108 $ok = $this->doService('basic-lis-readmembershipsforcontext', $url, $params,
1109 'https://purl.imsglobal.org/spec/lti-ext/scope/memberships');
1110 }
1111 if ($ok) {
1112 $this->groupSets = array();
1113 $this->groups = array();
1114 if (isset($this->extNodes['memberships'])) {
1115 $memberships = $this->extNodes['memberships'];
1116 } elseif (isset($this->extNodes['members'])) {
1117 $memberships = $this->extNodes['members'];
1118 } else {
1119 $ok = false;
1120 }
1121 }
1122 if ($ok) {
1123 if (!isset($memberships['member'])) {
1124 $members = array();
1125 } elseif (!isset($memberships['member'][0])) {
1126 $members = array();
1127 $members[0] = $memberships['member'];
1128 } else {
1129 $members = $memberships['member'];
1130 }
1131
1132 for ($i = 0; $i < count($members); $i++) {
1133
1134 $userresult = UserResult::fromResourceLink($this, $members[$i]['user_id']);
1135
1136// Set the user name
1137 $firstname = (isset($members[$i]['person_name_given'])) ? $members[$i]['person_name_given'] : '';
1138 $middlename = (isset($members[$i]['person_name_middle'])) ? $members[$i]['person_name_middle'] : '';
1139 $lastname = (isset($members[$i]['person_name_family'])) ? $members[$i]['person_name_family'] : '';
1140 $fullname = (isset($members[$i]['person_name_full'])) ? $members[$i]['person_name_full'] : '';
1141 $userresult->setNames($firstname, $lastname, $fullname);
1142
1143// Set the sourcedId
1144 if (isset($members[$i]['person_sourcedid'])) {
1145 $userresult->sourcedId = $members[$i]['person_sourcedid'];
1146 }
1147
1148// Set the user email
1149 $email = (isset($members[$i]['person_contact_email_primary'])) ? $members[$i]['person_contact_email_primary'] : '';
1150 $userresult->setEmail($email, $this->getPlatform()->defaultEmail);
1151
1152// Set the user roles
1153 if (isset($members[$i]['roles'])) {
1154 $userresult->roles = Tool::parseRoles($members[$i]['roles']);
1155 }
1156
1157// Set the user groups
1158 if (!isset($members[$i]['groups']['group'])) {
1159 $groups = array();
1160 } elseif (!isset($members[$i]['groups']['group'][0])) {
1161 $groups = array();
1162 $groups[0] = $members[$i]['groups']['group'];
1163 } else {
1164 $groups = $members[$i]['groups']['group'];
1165 }
1166 for ($j = 0; $j < count($groups); $j++) {
1167 $group = $groups[$j];
1168 if (isset($group['set'])) {
1169 $set_id = $group['set']['id'];
1170 if (!isset($this->groupSets[$set_id])) {
1171 $this->groupSets[$set_id] = array('title' => $group['set']['title'], 'groups' => array(),
1172 'num_members' => 0, 'num_staff' => 0, 'num_learners' => 0);
1173 }
1174 $this->groupSets[$set_id]['num_members']++;
1175 if ($userresult->isStaff()) {
1176 $this->groupSets[$set_id]['num_staff']++;
1177 }
1178 if ($userresult->isLearner()) {
1179 $this->groupSets[$set_id]['num_learners']++;
1180 }
1181 if (!in_array($group['id'], $this->groupSets[$set_id]['groups'])) {
1182 $this->groupSets[$set_id]['groups'][] = $group['id'];
1183 }
1184 $this->groups[$group['id']] = array('title' => $group['title'], 'set' => $set_id);
1185 } else {
1186 $this->groups[$group['id']] = array('title' => $group['title']);
1187 }
1188 $userresult->groups[] = $group['id'];
1189 }
1190 if (isset($members[$i]['lis_result_sourcedid'])) {
1191 $userresult->ltiResultSourcedId = $members[$i]['lis_result_sourcedid'];
1192 }
1193 $userResults[] = $userresult;
1194 }
1195 } else {
1196 $userResults = false;
1197 }
1198 $ok = $userResults !== false;
1199 }
1200 if (!$ok && $hasApiHook) {
1201 $className = $this->getApiHook(self::$MEMBERSHIPS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode());
1202 $hook = new $className($this);
1203 $userResults = $hook->getMemberships($withGroups);
1204 $ok = $userResults !== false;
1205 }
1206 if ($ok) {
1207 $oldUsers = $this->getUserResultSourcedIDs(true, Tool::ID_SCOPE_RESOURCE);
1208 foreach ($userResults as $userresult) {
1209// If a result sourcedid is provided save the user
1210 if (!empty($userresult->ltiResultSourcedId)) {
1211 $userresult->save();
1212 }
1213// Remove old user (if it exists)
1214 unset($oldUsers[$userresult->getId(Tool::ID_SCOPE_RESOURCE)]);
1215 }
1216// Delete any old users which were not in the latest list from the platform
1217 foreach ($oldUsers as $id => $userresult) {
1218 $userresult->delete();
1219 }
1220 }
1221
1222 return $userResults;
1223 }
1224
1236 public function getUserResultSourcedIDs($localOnly = false, $idScope = null)
1237 {
1238 return $this->getDataConnector()->getUserResultSourcedIDsResourceLink($this, $localOnly, $idScope);
1239 }
1240
1246 public function getShares()
1247 {
1248 return $this->getDataConnector()->getSharesResourceLink($this);
1249 }
1250
1260 public function getLineItems($resourceId = null, $tag = null, $limit = null)
1261 {
1262 $lineItems = false;
1263 $this->extRequest = '';
1264 $this->extRequestHeaders = '';
1265 $this->extResponse = '';
1266 $this->extResponseHeaders = '';
1267 $this->lastServiceRequest = null;
1268 $lineItemService = $this->getLineItemService();
1269 if (!empty($lineItemService)) {
1270 $lineItems = $lineItemService->getAll($this->ltiResourceLinkId, $resourceId, $tag, $limit);
1271 $http = $lineItemService->getHttpMessage();
1272 $this->extResponse = $http->response;
1273 $this->extResponseHeaders = $http->responseHeaders;
1274 $this->extRequest = $http->request;
1275 $this->extRequestHeaders = $http->requestHeaders;
1276 $this->lastServiceRequest = $http;
1277 }
1278
1279 return $lineItems;
1280 }
1281
1289 public function createLineItem($lineItem)
1290 {
1291 $ok = false;
1292 $lineItemService = $this->getLineItemService();
1293 if (!empty($lineItemService)) {
1294 $lineItem->ltiResourceLinkId = $this->ltiResourceLinkId;
1295 $ok = $lineItemService->createLineItem($lineItem);
1296 }
1297
1298 return $ok;
1299 }
1300
1308 public function getOutcomes($limit = null)
1309 {
1310 $outcomes = false;
1311 $this->extRequest = '';
1312 $this->extRequestHeaders = '';
1313 $this->extResponse = '';
1314 $this->extResponseHeaders = '';
1315 $this->lastServiceRequest = null;
1316 $url = $this->getSetting('custom_lineitem_url');
1317 if (!empty($url)) {
1318 $resultService = new Service\Result($this->getPlatform(), $url);
1319 $outcomes = $resultService->getAll($limit);
1320 $http = $resultService->getHttpMessage();
1321 $this->extResponse = $http->response;
1322 $this->extResponseHeaders = $http->responseHeaders;
1323 $this->extRequest = $http->request;
1324 $this->extRequestHeaders = $http->requestHeaders;
1325 $this->lastServiceRequest = $http;
1326 }
1327
1328 return $outcomes;
1329 }
1330
1340 public function doAssessmentControlAction($assessmentControlAction, $user, $attemptNumber)
1341 {
1342 $status = false;
1343 $this->extRequest = '';
1344 $this->extRequestHeaders = '';
1345 $this->extResponse = '';
1346 $this->extResponseHeaders = '';
1347 $this->lastServiceRequest = null;
1348 $url = $this->getSetting('custom_ap_acs_url');
1349 if (!empty($url)) {
1350 $assessmentControlService = new Service\AssessmentControl($this, $url);
1351 $status = $assessmentControlService->submitAction($assessmentControlAction, $user, $attemptNumber);
1352 $http = $assessmentControlService->getHttpMessage();
1353 $this->extResponse = $http->response;
1354 $this->extResponseHeaders = $http->responseHeaders;
1355 $this->extRequest = $http->request;
1356 $this->extRequestHeaders = $http->requestHeaders;
1357 $this->lastServiceRequest = $http;
1358 }
1359
1360 return $status;
1361 }
1362
1375 public static function fromConsumer($consumer, $ltiResourceLinkId, $tempId = null)
1376 {
1377 Util::logDebug('Method ceLTIc\LTI\ResourceLink::fromConsumer() has been deprecated; please use ceLTIc\LTI\ResourceLink::fromPlatform() instead.',
1378 true);
1379 return self::fromPlatform($consumer, $ltiResourceLinkId, $tempId);
1380 }
1381
1391 public static function fromPlatform($platform, $ltiResourceLinkId, $tempId = null)
1392 {
1393 $resourceLink = new ResourceLink();
1394 $resourceLink->platform = $platform;
1395 $resourceLink->dataConnector = $platform->getDataConnector();
1396 $resourceLink->ltiResourceLinkId = $ltiResourceLinkId;
1397 if (!empty($ltiResourceLinkId)) {
1398 $resourceLink->load();
1399 if (is_null($resourceLink->id) && !empty($tempId)) {
1400 $resourceLink->ltiResourceLinkId = $tempId;
1401 $resourceLink->load();
1402 $resourceLink->ltiResourceLinkId = $ltiResourceLinkId;
1403 }
1404 }
1405
1406 return $resourceLink;
1407 }
1408
1418 public static function fromContext($context, $ltiResourceLinkId, $tempId = null)
1419 {
1420 $resourceLink = new ResourceLink();
1421 $resourceLink->setContext($context);
1422 $resourceLink->dataConnector = $context->getDataConnector();
1423 $resourceLink->ltiResourceLinkId = $ltiResourceLinkId;
1424 if (!empty($ltiResourceLinkId)) {
1425 $resourceLink->load();
1426 if (is_null($resourceLink->id) && !empty($tempId)) {
1427 $resourceLink->ltiResourceLinkId = $tempId;
1428 $resourceLink->load();
1429 $resourceLink->ltiResourceLinkId = $ltiResourceLinkId;
1430 }
1431 $resourceLink->setContext($context); // Ensure context remains set
1432 }
1433
1434 return $resourceLink;
1435 }
1436
1445 public static function fromRecordId($id, $dataConnector)
1446 {
1447 $resourceLink = new ResourceLink();
1448 $resourceLink->dataConnector = $dataConnector;
1449 $resourceLink->load($id);
1450
1451 return $resourceLink;
1452 }
1453
1454###
1455### PRIVATE METHODS
1456###
1457
1465 private function load($id = null)
1466 {
1467 $this->initialize();
1468 $this->id = $id;
1469
1470 return $this->getDataConnector()->loadResourceLink($this);
1471 }
1472
1481 private function checkValueType($ltiOutcome, $supportedTypes = null)
1482 {
1483 if (empty($supportedTypes)) {
1484 $supportedTypes = explode(',',
1485 str_replace(' ', '', strtolower($this->getSetting('ext_ims_lis_resultvalue_sourcedids', self::EXT_TYPE_DECIMAL))));
1486 }
1487 $type = $ltiOutcome->type;
1488 $value = $ltiOutcome->getValue();
1489// Check whether the type is supported or there is no value
1490 $ok = in_array($type, $supportedTypes) || empty($value);
1491 if (!$ok) {
1492// Convert numeric values to decimal
1493 if ($type === self::EXT_TYPE_PERCENTAGE) {
1494 if (substr($value, -1) === '%') {
1495 $value = substr($value, 0, -1);
1496 }
1497 $ok = is_numeric($value) && ($value >= 0) && ($value <= 100);
1498 if ($ok) {
1499 $ltiOutcome->setValue($value / 100);
1500 $ltiOutcome->type = self::EXT_TYPE_DECIMAL;
1501 }
1502 } elseif ($type === self::EXT_TYPE_RATIO) {
1503 $parts = explode('/', $value, 2);
1504 $ok = (count($parts) === 2) && is_numeric($parts[0]) && is_numeric($parts[1]) && ($parts[0] >= 0) && ($parts[1] > 0);
1505 if ($ok) {
1506 $ltiOutcome->setValue($parts[0] / $parts[1]);
1507 $ltiOutcome->type = self::EXT_TYPE_DECIMAL;
1508 }
1509// Convert letter_af to letter_af_plus or text
1510 } elseif ($type === self::EXT_TYPE_LETTER_AF) {
1511 if (in_array(self::EXT_TYPE_LETTER_AF_PLUS, $supportedTypes)) {
1512 $ok = true;
1513 $ltiOutcome->type = self::EXT_TYPE_LETTER_AF_PLUS;
1514 } elseif (in_array(self::EXT_TYPE_TEXT, $supportedTypes)) {
1515 $ok = true;
1516 $ltiOutcome->type = self::EXT_TYPE_TEXT;
1517 }
1518// Convert letter_af_plus to letter_af or text
1519 } elseif ($type === self::EXT_TYPE_LETTER_AF_PLUS) {
1520 if (in_array(self::EXT_TYPE_LETTER_AF, $supportedTypes) && (strlen($value) === 1)) {
1521 $ok = true;
1522 $ltiOutcome->type = self::EXT_TYPE_LETTER_AF;
1523 } elseif (in_array(self::EXT_TYPE_TEXT, $supportedTypes)) {
1524 $ok = true;
1525 $ltiOutcome->type = self::EXT_TYPE_TEXT;
1526 }
1527// Convert text to decimal
1528 } elseif ($type === self::EXT_TYPE_TEXT) {
1529 $ok = is_numeric($value) && ($value >= 0) && ($value <= 1);
1530 if ($ok) {
1531 $ltiOutcome->type = self::EXT_TYPE_DECIMAL;
1532 } elseif (substr($value, -1) === '%') {
1533 $value = substr($value, 0, -1);
1534 $ok = is_numeric($value) && ($value >= 0) && ($value <= 100);
1535 if ($ok) {
1536 if (in_array(self::EXT_TYPE_PERCENTAGE, $supportedTypes)) {
1537 $ltiOutcome->type = self::EXT_TYPE_PERCENTAGE;
1538 } else {
1539 $ltiOutcome->setValue($value / 100);
1540 $ltiOutcome->type = self::EXT_TYPE_DECIMAL;
1541 }
1542 }
1543 }
1544 }
1545 }
1546
1547 return $ok;
1548 }
1549
1560 private function doService($type, $url, $params, $scope)
1561 {
1562 $ok = false;
1563 $this->extRequest = '';
1564 $this->extRequestHeaders = '';
1565 $this->extResponse = '';
1566 $this->extResponseHeaders = '';
1567 $this->lastServiceRequest = null;
1568 if (!empty($url)) {
1569 $params['lti_version'] = $this->getPlatform()->ltiVersion;
1570 $params['lti_message_type'] = $type;
1571 $retry = false;
1572 $newToken = false;
1573 if ($this->getPlatform()->useOAuth1()) {
1574 $paramstosign = $params;
1575 } else {
1576 $paramstosign = null;
1577 $accessToken = $this->platform->getAccessToken();
1578 $retry = true;
1579 if (empty($accessToken)) {
1580 $accessToken = new AccessToken($this->platform);
1581 $this->platform->setAccessToken($accessToken);
1582 }
1583 if (!$accessToken->hasScope($scope) && (empty(Tool::$defaultTool) || !in_array($scope,
1584 Tool::$defaultTool->requiredScopes))) {
1585 $accessToken->expires = time();
1586 $accessToken->get($scope, true);
1587 $this->platform->setAccessToken($accessToken);
1588 $newToken = true;
1589 }
1590 }
1591 do {
1592// Add message signature
1593 $signed = $this->platform->addSignature($url, $paramstosign, 'POST', 'application/x-www-form-urlencoded');
1594// Connect to platform
1595 if (is_array($signed)) {
1596 $http = new HttpMessage($url, 'POST', $signed, 'Accept: application/xml');
1597 } else {
1598 $http = new HttpMessage($url, 'POST', $params, "{$signed}\nAccept: application/xml");
1599 }
1600 if ($http->send()) {
1601// Parse XML response
1602 $this->extResponse = $http->response;
1603 $this->extResponseHeaders = $http->responseHeaders;
1604 try {
1605 $this->extDoc = new DOMDocument();
1606 $this->extDoc->loadXML($http->response);
1607 $this->extNodes = $this->domnodeToArray($this->extDoc->documentElement);
1608 if (isset($this->extNodes['statusinfo']['codemajor']) && ($this->extNodes['statusinfo']['codemajor'] === 'Success')) {
1609 $ok = true;
1610 }
1611 } catch (\Exception $e) {
1612
1613 }
1614 }
1615 $retry = $retry && !$newToken && !$ok;
1616 if ($retry) { // Obtain a new access token just for the required scope
1617 $accessToken = $this->platform->getAccessToken();
1618 $accessToken->expires = time();
1619 $accessToken->get($scope, true);
1620 $this->platform->setAccessToken($accessToken);
1621 $newToken = true;
1622 }
1623 } while ($retry);
1624 $this->extRequest = $http->request;
1625 $this->extRequestHeaders = $http->requestHeaders;
1626 $this->lastServiceRequest = $http;
1627 }
1628
1629 return $ok;
1630 }
1631
1640 private function doResultService($userResult, $url)
1641 {
1642 $outcome = null;
1643 $this->extRequest = '';
1644 $this->extRequestHeaders = '';
1645 $this->extResponse = '';
1646 $this->extResponseHeaders = '';
1647 $this->lastServiceRequest = null;
1648 if (!empty($url)) {
1649 $resultService = new Service\Result($this->getPlatform(), $url);
1650 $outcome = $resultService->get($userResult);
1651 $http = $resultService->getHttpMessage();
1652 $this->extResponse = $http->response;
1653 $this->extResponseHeaders = $http->responseHeaders;
1654 $this->extRequest = $http->request;
1655 $this->extRequestHeaders = $http->requestHeaders;
1656 $this->lastServiceRequest = $http;
1657 }
1658
1659 return $outcome;
1660 }
1661
1671 private function doScoreService($ltiOutcome, $userResult, $url)
1672 {
1673 $ok = false;
1674 $this->extRequest = '';
1675 $this->extRequestHeaders = '';
1676 $this->extResponse = '';
1677 $this->extResponseHeaders = '';
1678 $this->lastServiceRequest = null;
1679 if (!empty($url)) {
1680 $scoreService = new Service\Score($this->getPlatform(), $url);
1681 $scoreService->submit($ltiOutcome, $userResult);
1682 $http = $scoreService->getHttpMessage();
1683 $this->extResponse = $http->response;
1684 $this->extResponseHeaders = $http->responseHeaders;
1685 $ok = $http->ok;
1686 $this->extRequest = $http->request;
1687 $this->extRequestHeaders = $http->requestHeaders;
1688 $this->lastServiceRequest = $http;
1689 }
1690
1691 return $ok;
1692 }
1693
1703 private function doLTI11Service($type, $url, $xml)
1704 {
1705 $ok = false;
1706 $this->extRequest = '';
1707 $this->extRequestHeaders = '';
1708 $this->extResponse = '';
1709 $this->extResponseHeaders = '';
1710 $this->lastServiceRequest = null;
1711 if (!empty($url)) {
1712 $id = uniqid();
1713 $xmlRequest = <<< EOD
1714<?xml version = "1.0" encoding = "UTF-8"?>
1715<imsx_POXEnvelopeRequest xmlns = "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
1716 <imsx_POXHeader>
1717 <imsx_POXRequestHeaderInfo>
1718 <imsx_version>V1.0</imsx_version>
1719 <imsx_messageIdentifier>{$id}</imsx_messageIdentifier>
1720 </imsx_POXRequestHeaderInfo>
1721 </imsx_POXHeader>
1722 <imsx_POXBody>
1723 <{$type}Request>
1724{$xml}
1725 </{$type}Request>
1726 </imsx_POXBody>
1727</imsx_POXEnvelopeRequest>
1728EOD;
1729 $scope = 'https://purl.imsglobal.org/spec/lti-bo/scope/basicoutcome';
1730 $retry = false;
1731 $newToken = false;
1732 if (!$this->getPlatform()->useOAuth1()) {
1733 $accessToken = $this->platform->getAccessToken();
1734 $retry = true;
1735 if (empty($accessToken)) {
1736 $accessToken = new AccessToken($this->platform);
1737 $this->platform->setAccessToken($accessToken);
1738 }
1739 if (!$accessToken->hasScope($scope) && (empty(Tool::$defaultTool) || !in_array($scope,
1740 Tool::$defaultTool->requiredScopes))) {
1741 $accessToken->expires = time();
1742 $accessToken->get($scope, true);
1743 $this->platform->setAccessToken($accessToken);
1744 $newToken = true;
1745 }
1746 }
1747 do {
1748// Add message signature
1749 $header = $this->getPlatform()->addSignature($url, $xmlRequest, 'POST', 'application/xml');
1750 $header .= "\nAccept: application/xml";
1751// Connect to platform
1752 $http = new HttpMessage($url, 'POST', $xmlRequest, $header);
1753 if ($http->send()) {
1754// Parse XML response
1755 $this->extResponse = $http->response;
1756 $this->extResponseHeaders = $http->responseHeaders;
1757 try {
1758 $this->extDoc = new DOMDocument();
1759 $this->extDoc->loadXML($http->response);
1760 $this->extNodes = $this->domnodeToArray($this->extDoc->documentElement);
1761 if (isset($this->extNodes['imsx_POXHeader']['imsx_POXResponseHeaderInfo']['imsx_statusInfo']['imsx_codeMajor']) &&
1762 ($this->extNodes['imsx_POXHeader']['imsx_POXResponseHeaderInfo']['imsx_statusInfo']['imsx_codeMajor'] === 'success')) {
1763 $ok = true;
1764 }
1765 } catch (\Exception $e) {
1766
1767 }
1768 }
1769 $retry = $retry && !$newToken && !$ok;
1770 if ($retry) { // Obtain a new access token just for the required scope
1771 $accessToken = $this->platform->getAccessToken();
1772 $accessToken->expires = time();
1773 $accessToken->get($scope, true);
1774 $this->platform->setAccessToken($accessToken);
1775 $newToken = true;
1776 }
1777 } while ($retry);
1778 $this->extRequest = $http->request;
1779 $this->extRequestHeaders = $http->requestHeaders;
1780 $this->lastServiceRequest = $http;
1781 }
1782
1783 return $ok;
1784 }
1785
1791 private function getLineItemService()
1792 {
1793 $url = $this->getSetting('custom_lineitems_url');
1794 if (!empty($url)) {
1795 $lineItemService = new Service\LineItem($this->getPlatform(), $url);
1796 } else {
1797 $lineItemService = false;
1798 }
1799
1800 return $lineItemService;
1801 }
1802
1810 private function domnodeToArray($node)
1811 {
1812 $output = array();
1813 switch ($node->nodeType) {
1814 case XML_CDATA_SECTION_NODE:
1815 case XML_TEXT_NODE:
1816 $output = trim($node->textContent);
1817 break;
1818 case XML_ELEMENT_NODE:
1819 for ($i = 0; $i < $node->childNodes->length; $i++) {
1820 $child = $node->childNodes->item($i);
1821 $v = $this->domnodeToArray($child);
1822 if (isset($child->tagName)) {
1823 $name = explode(':', $child->tagName, 2); // Remove any namespace
1824 $output[$name[count($name) - 1]][] = $v;
1825 } else {
1826 $s = (string) $v;
1827 if (!empty($s)) {
1828 $output = $s;
1829 }
1830 }
1831 }
1832 if (is_array($output)) {
1833 if ($node->hasAttributes()) {
1834 foreach ($node->attributes as $attrNode) {
1835 $output['@attributes'][$attrNode->name] = (string) $attrNode->value;
1836 }
1837 }
1838 foreach ($output as $t => $v) {
1839 if (is_array($v) && (count($v) === 1) && ($t !== '@attributes')) {
1840 $output[$t] = $v[0];
1841 }
1842 }
1843 }
1844 break;
1845 }
1846
1847 return $output;
1848 }
1849
1850}
Class to represent an HTTP message.
Trait to handle API hook registrations.
Definition ApiHook.php:13
static fromRecordId($id, $dataConnector)
Load the context from the database.
Definition Context.php:661
Class to provide a connection to a persistent store for LTI objects.
Class to represent an HTTP message request.
Class to represent a line-item.
Definition LineItem.php:15
static fromRecordId($id, $dataConnector)
Load the platform from the database by its record ID.
Definition Platform.php:534
Class to implement the Assessment Control service.
Class to implement the Membership service.
Class to implement the Result service.
Definition Result.php:17
static $SCOPE
Access scope.
Definition Result.php:24
Class to implement the Score service.
Definition Score.php:17
static $SCOPE
Access scope.
Definition Score.php:24
Class to implement a service.
Definition Service.php:19
Class to implement the Tool Settings service.
const MODE_CURRENT_LEVEL
Settings at current level mode.
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
const ID_SCOPE_RESOURCE
Prefix the ID with the consumer key and resource ID.
Definition Tool.php:49
static $defaultTool
Default tool for use with service requests.
Definition Tool.php:294
static fromResourceLink($resourceLink, $ltiUserId)
Class constructor from resource link.
static logDebug($message, $showSource=false)
Log a debug message.
Definition Util.php:274