Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * This file contains a class definition for the Memberships service 19 * 20 * @package ltiservice_memberships 21 * @copyright 2015 Vital Source Technologies http://vitalsource.com 22 * @author Stephen Vickers 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 namespace ltiservice_memberships\local\service; 27 28 defined('MOODLE_INTERNAL') || die(); 29 30 /** 31 * A service implementing Memberships. 32 * 33 * @package ltiservice_memberships 34 * @since Moodle 3.0 35 * @copyright 2015 Vital Source Technologies http://vitalsource.com 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 */ 38 class memberships extends \mod_lti\local\ltiservice\service_base { 39 40 /** Default prefix for context-level roles */ 41 const CONTEXT_ROLE_PREFIX = 'http://purl.imsglobal.org/vocab/lis/v2/membership#'; 42 /** Context-level role for Instructor */ 43 const CONTEXT_ROLE_INSTRUCTOR = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor'; 44 /** Context-level role for Learner */ 45 const CONTEXT_ROLE_LEARNER = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'; 46 /** Capability used to identify Instructors */ 47 const INSTRUCTOR_CAPABILITY = 'moodle/course:manageactivities'; 48 /** Always include field */ 49 const ALWAYS_INCLUDE_FIELD = 1; 50 /** Allow the instructor to decide if included */ 51 const DELEGATE_TO_INSTRUCTOR = 2; 52 /** Instructor chose to include field */ 53 const INSTRUCTOR_INCLUDED = 1; 54 /** Instructor delegated and approved for include */ 55 const INSTRUCTOR_DELEGATE_INCLUDED = array(self::DELEGATE_TO_INSTRUCTOR && self::INSTRUCTOR_INCLUDED); 56 /** Scope for reading membership data */ 57 const SCOPE_MEMBERSHIPS_READ = 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly'; 58 59 /** 60 * Class constructor. 61 */ 62 public function __construct() { 63 64 parent::__construct(); 65 $this->id = 'memberships'; 66 $this->name = get_string($this->get_component_id(), $this->get_component_id()); 67 68 } 69 70 /** 71 * Get the resources for this service. 72 * 73 * @return array 74 */ 75 public function get_resources() { 76 77 if (empty($this->resources)) { 78 $this->resources = array(); 79 $this->resources[] = new \ltiservice_memberships\local\resources\contextmemberships($this); 80 $this->resources[] = new \ltiservice_memberships\local\resources\linkmemberships($this); 81 } 82 83 return $this->resources; 84 85 } 86 87 /** 88 * Get the scope(s) permitted for the tool relevant to this service. 89 * 90 * @return array 91 */ 92 public function get_permitted_scopes() { 93 94 $scopes = array(); 95 $ok = !empty($this->get_type()); 96 if ($ok && isset($this->get_typeconfig()[$this->get_component_id()]) && 97 ($this->get_typeconfig()[$this->get_component_id()] == parent::SERVICE_ENABLED)) { 98 $scopes[] = self::SCOPE_MEMBERSHIPS_READ; 99 } 100 101 return $scopes; 102 103 } 104 105 /** 106 * Get the scope(s) defined by this service. 107 * 108 * @return array 109 */ 110 public function get_scopes() { 111 return [self::SCOPE_MEMBERSHIPS_READ]; 112 } 113 114 /** 115 * Get the JSON for members. 116 * 117 * @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request 118 * @param \context_course $context Course context 119 * @param string $contextid Course ID 120 * @param object $tool Tool instance object 121 * @param string $role User role requested (empty if none) 122 * @param int $limitfrom Position of first record to be returned 123 * @param int $limitnum Maximum number of records to be returned 124 * @param object $lti LTI instance record 125 * @param \core_availability\info_module $info Conditional availability information 126 * for LTI instance (null if context-level request) 127 * 128 * @return string 129 * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. 130 * @see memberships::get_members_json($resource, $context, $course, $role, $limitfrom, $limitnum, $lti, $info, $response) 131 */ 132 public static function get_users_json($resource, $context, $contextid, $tool, $role, $limitfrom, $limitnum, $lti, $info) { 133 global $DB; 134 135 debugging('get_users_json() has been deprecated, ' . 136 'please use memberships::get_members_json() instead.', DEBUG_DEVELOPER); 137 138 $course = $DB->get_record('course', array('id' => $contextid), 'id,shortname,fullname', IGNORE_MISSING); 139 140 $memberships = new memberships(); 141 $memberships->check_tool($tool->id, null, array(self::SCOPE_MEMBERSHIPS_READ)); 142 143 $response = new \mod_lti\local\ltiservice\response(); 144 145 $json = $memberships->get_members_json($resource, $context, $course, $role, $limitfrom, $limitnum, $lti, $info, $response); 146 147 return $json; 148 } 149 150 /** 151 * Get the JSON for members. 152 * 153 * @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request 154 * @param \context_course $context Course context 155 * @param \course $course Course 156 * @param string $role User role requested (empty if none) 157 * @param int $limitfrom Position of first record to be returned 158 * @param int $limitnum Maximum number of records to be returned 159 * @param object $lti LTI instance record 160 * @param \core_availability\info_module $info Conditional availability information 161 * for LTI instance (null if context-level request) 162 * @param \mod_lti\local\ltiservice\response $response Response object for the request 163 * 164 * @return string 165 */ 166 public function get_members_json($resource, $context, $course, $role, $limitfrom, $limitnum, $lti, $info, $response) { 167 168 $withcapability = ''; 169 $exclude = array(); 170 if (!empty($role)) { 171 if ((strpos($role, 'http://') !== 0) && (strpos($role, 'https://') !== 0)) { 172 $role = self::CONTEXT_ROLE_PREFIX . $role; 173 } 174 if ($role === self::CONTEXT_ROLE_INSTRUCTOR) { 175 $withcapability = self::INSTRUCTOR_CAPABILITY; 176 } else if ($role === self::CONTEXT_ROLE_LEARNER) { 177 $exclude = array_keys(get_enrolled_users($context, self::INSTRUCTOR_CAPABILITY, 0, 'u.id', 178 null, null, null, true)); 179 } 180 } 181 $users = get_enrolled_users($context, $withcapability, 0, 'u.*', null, 0, 0, true); 182 if (($response->get_accept() === 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json') || 183 (($response->get_accept() !== 'application/vnd.ims.lis.v2.membershipcontainer+json') && 184 ($this->get_type()->ltiversion === LTI_VERSION_1P3))) { 185 $json = $this->users_to_json($resource, $users, $course, $exclude, $limitfrom, $limitnum, $lti, $info, $response); 186 } else { 187 $json = $this->users_to_jsonld($resource, $users, $course->id, $exclude, $limitfrom, $limitnum, $lti, $info, $response); 188 } 189 190 return $json; 191 } 192 193 /** 194 * Get the JSON-LD representation of the users. 195 * 196 * Note that when a limit is set and the exclude array is not empty, then the number of memberships 197 * returned may be less than the limit. 198 * 199 * @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request 200 * @param array $users Array of user records 201 * @param string $contextid Course ID 202 * @param array $exclude Array of user records to be excluded from the response 203 * @param int $limitfrom Position of first record to be returned 204 * @param int $limitnum Maximum number of records to be returned 205 * @param object $lti LTI instance record 206 * @param \core_availability\info_module $info Conditional availability information 207 * for LTI instance (null if context-level request) 208 * @param \mod_lti\local\ltiservice\response $response Response object for the request 209 * 210 * @return string 211 */ 212 private function users_to_jsonld($resource, $users, $contextid, $exclude, $limitfrom, $limitnum, 213 $lti, $info, $response) { 214 global $DB; 215 216 $tool = $this->get_type(); 217 $toolconfig = $this->get_typeconfig(); 218 $arrusers = [ 219 '@context' => 'http://purl.imsglobal.org/ctx/lis/v2/MembershipContainer', 220 '@type' => 'Page', 221 '@id' => $resource->get_endpoint(), 222 ]; 223 224 $arrusers['pageOf'] = [ 225 '@type' => 'LISMembershipContainer', 226 'membershipSubject' => [ 227 '@type' => 'Context', 228 'contextId' => $contextid, 229 'membership' => [] 230 ] 231 ]; 232 233 $enabledcapabilities = lti_get_enabled_capabilities($tool); 234 $islti2 = $tool->toolproxyid > 0; 235 $n = 0; 236 $more = false; 237 foreach ($users as $user) { 238 if (in_array($user->id, $exclude)) { 239 continue; 240 } 241 if (!empty($info) && !$info->is_user_visible($info->get_course_module(), $user->id)) { 242 continue; 243 } 244 $n++; 245 if ($limitnum > 0) { 246 if ($n <= $limitfrom) { 247 continue; 248 } 249 if (count($arrusers['pageOf']['membershipSubject']['membership']) >= $limitnum) { 250 $more = true; 251 break; 252 } 253 } 254 255 $member = new \stdClass(); 256 $member->{"@type" } = 'LISPerson'; 257 $membership = new \stdClass(); 258 $membership->status = 'Active'; 259 $membership->role = explode(',', lti_get_ims_role($user->id, null, $contextid, true)); 260 261 $instanceconfig = null; 262 if (!is_null($lti)) { 263 $instanceconfig = lti_get_type_config_from_instance($lti->id); 264 } 265 $isallowedlticonfig = self::is_allowed_field_set($toolconfig, $instanceconfig, 266 ['name' => 'sendname', 'email' => 'sendemailaddr']); 267 268 $includedcapabilities = [ 269 'User.id' => ['type' => 'id', 270 'member.field' => 'userId', 271 'source.value' => $user->id], 272 'Person.sourcedId' => ['type' => 'id', 273 'member.field' => 'sourcedId', 274 'source.value' => format_string($user->idnumber)], 275 'Person.name.full' => ['type' => 'name', 276 'member.field' => 'name', 277 'source.value' => format_string("{$user->firstname} {$user->lastname}")], 278 'Person.name.given' => ['type' => 'name', 279 'member.field' => 'givenName', 280 'source.value' => format_string($user->firstname)], 281 'Person.name.family' => ['type' => 'name', 282 'member.field' => 'familyName', 283 'source.value' => format_string($user->lastname)], 284 'Person.email.primary' => ['type' => 'email', 285 'member.field' => 'email', 286 'source.value' => format_string($user->email)], 287 'User.username' => ['type' => 'name', 288 'member.field' => 'ext_user_username', 289 'source.value' => format_string($user->username)] 290 ]; 291 292 if (!is_null($lti)) { 293 $message = new \stdClass(); 294 $message->message_type = 'basic-lti-launch-request'; 295 $conditions = array('courseid' => $contextid, 'itemtype' => 'mod', 296 'itemmodule' => 'lti', 'iteminstance' => $lti->id); 297 298 if (!empty($lti->servicesalt) && $DB->record_exists('grade_items', $conditions)) { 299 $message->lis_result_sourcedid = json_encode(lti_build_sourcedid($lti->id, 300 $user->id, 301 $lti->servicesalt, 302 $lti->typeid)); 303 // Not per specification but added to comply with earlier version of the service. 304 $member->resultSourcedId = $message->lis_result_sourcedid; 305 } 306 $membership->message = [$message]; 307 } 308 309 foreach ($includedcapabilities as $capabilityname => $capability) { 310 if ($islti2) { 311 if (in_array($capabilityname, $enabledcapabilities)) { 312 $member->{$capability['member.field']} = $capability['source.value']; 313 } 314 } else { 315 if (($capability['type'] === 'id') 316 || ($capability['type'] === 'name' && $isallowedlticonfig['name']) 317 || ($capability['type'] === 'email' && $isallowedlticonfig['email'])) { 318 $member->{$capability['member.field']} = $capability['source.value']; 319 } 320 } 321 } 322 323 $membership->member = $member; 324 325 $arrusers['pageOf']['membershipSubject']['membership'][] = $membership; 326 } 327 if ($more) { 328 $nextlimitfrom = $limitfrom + $limitnum; 329 $nextpage = "{$resource->get_endpoint()}?limit={$limitnum}&from={$nextlimitfrom}"; 330 if (!is_null($lti)) { 331 $nextpage .= "&rlid={$lti->id}"; 332 } 333 $arrusers['nextPage'] = $nextpage; 334 } 335 336 $response->set_content_type('application/vnd.ims.lis.v2.membershipcontainer+json'); 337 338 return json_encode($arrusers); 339 } 340 341 /** 342 * Get the NRP service JSON representation of the users. 343 * 344 * Note that when a limit is set and the exclude array is not empty, then the number of memberships 345 * returned may be less than the limit. 346 * 347 * @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request 348 * @param array $users Array of user records 349 * @param \course $course Course 350 * @param array $exclude Array of user records to be excluded from the response 351 * @param int $limitfrom Position of first record to be returned 352 * @param int $limitnum Maximum number of records to be returned 353 * @param object $lti LTI instance record 354 * @param \core_availability\info_module $info Conditional availability information for LTI instance 355 * @param \mod_lti\local\ltiservice\response $response Response object for the request 356 * 357 * @return string 358 */ 359 private function users_to_json($resource, $users, $course, $exclude, $limitfrom, $limitnum, 360 $lti, $info, $response) { 361 global $DB, $CFG; 362 363 $tool = $this->get_type(); 364 $toolconfig = $this->get_typeconfig(); 365 366 $context = new \stdClass(); 367 $context->id = $course->id; 368 $context->label = trim(html_to_text($course->shortname, 0)); 369 $context->title = trim(html_to_text($course->fullname, 0)); 370 371 $arrusers = [ 372 'id' => $resource->get_endpoint(), 373 'context' => $context, 374 'members' => [] 375 ]; 376 377 $islti2 = $tool->toolproxyid > 0; 378 $n = 0; 379 $more = false; 380 foreach ($users as $user) { 381 if (in_array($user->id, $exclude)) { 382 continue; 383 } 384 if (!empty($info) && !$info->is_user_visible($info->get_course_module(), $user->id)) { 385 continue; 386 } 387 $n++; 388 if ($limitnum > 0) { 389 if ($n <= $limitfrom) { 390 continue; 391 } 392 if (count($arrusers['members']) >= $limitnum) { 393 $more = true; 394 break; 395 } 396 } 397 398 $member = new \stdClass(); 399 $member->status = 'Active'; 400 $member->roles = explode(',', lti_get_ims_role($user->id, null, $course->id, true)); 401 402 $instanceconfig = null; 403 if (!is_null($lti)) { 404 $instanceconfig = lti_get_type_config_from_instance($lti->id); 405 } 406 if (!$islti2) { 407 $isallowedlticonfig = self::is_allowed_field_set($toolconfig, $instanceconfig, 408 ['name' => 'sendname', 'givenname' => 'sendname', 'familyname' => 'sendname', 409 'email' => 'sendemailaddr']); 410 } else { 411 $isallowedlticonfig = self::is_allowed_capability_set($tool, 412 ['name' => 'Person.name.full', 'givenname' => 'Person.name.given', 413 'familyname' => 'Person.name.family', 'email' => 'Person.email.primary']); 414 } 415 $includedcapabilities = [ 416 'User.id' => ['type' => 'id', 417 'member.field' => 'user_id', 418 'source.value' => $user->id], 419 'Person.sourcedId' => ['type' => 'id', 420 'member.field' => 'lis_person_sourcedid', 421 'source.value' => format_string($user->idnumber)], 422 'Person.name.full' => ['type' => 'name', 423 'member.field' => 'name', 424 'source.value' => format_string("{$user->firstname} {$user->lastname}")], 425 'Person.name.given' => ['type' => 'givenname', 426 'member.field' => 'given_name', 427 'source.value' => format_string($user->firstname)], 428 'Person.name.family' => ['type' => 'familyname', 429 'member.field' => 'family_name', 430 'source.value' => format_string($user->lastname)], 431 'Person.email.primary' => ['type' => 'email', 432 'member.field' => 'email', 433 'source.value' => format_string($user->email)], 434 'User.username' => ['type' => 'name', 435 'member.field' => 'ext_user_username', 436 'source.value' => format_string($user->username)], 437 ]; 438 439 if (!is_null($lti)) { 440 $message = new \stdClass(); 441 $message->{'https://purl.imsglobal.org/spec/lti/claim/message_type'} = 'LtiResourceLinkRequest'; 442 $conditions = array('courseid' => $course->id, 'itemtype' => 'mod', 443 'itemmodule' => 'lti', 'iteminstance' => $lti->id); 444 445 if (!empty($lti->servicesalt) && $DB->record_exists('grade_items', $conditions)) { 446 $basicoutcome = new \stdClass(); 447 $basicoutcome->lis_result_sourcedid = json_encode(lti_build_sourcedid($lti->id, 448 $user->id, 449 $lti->servicesalt, 450 $lti->typeid)); 451 // Add outcome service URL. 452 $serviceurl = new \moodle_url('/mod/lti/service.php'); 453 $serviceurl = $serviceurl->out(); 454 $forcessl = false; 455 if (!empty($CFG->mod_lti_forcessl)) { 456 $forcessl = true; 457 } 458 if ((isset($toolconfig['forcessl']) && ($toolconfig['forcessl'] == '1')) or $forcessl) { 459 $serviceurl = lti_ensure_url_is_https($serviceurl); 460 } 461 $basicoutcome->lis_outcome_service_url = $serviceurl; 462 $message->{'https://purl.imsglobal.org/spec/lti-bo/claim/basicoutcome'} = $basicoutcome; 463 } 464 $member->message = [$message]; 465 } 466 467 foreach ($includedcapabilities as $capabilityname => $capability) { 468 if (($capability['type'] === 'id') || $isallowedlticonfig[$capability['type']]) { 469 $member->{$capability['member.field']} = $capability['source.value']; 470 } 471 } 472 473 $arrusers['members'][] = $member; 474 } 475 if ($more) { 476 $nextlimitfrom = $limitfrom + $limitnum; 477 $nextpage = "{$resource->get_endpoint()}?limit={$limitnum}&from={$nextlimitfrom}"; 478 if (!is_null($lti)) { 479 $nextpage .= "&rlid={$lti->id}"; 480 } 481 $response->add_additional_header("Link: <{$nextpage}>; rel=\"next\""); 482 } 483 484 $response->set_content_type('application/vnd.ims.lti-nrps.v2.membershipcontainer+json'); 485 486 return json_encode($arrusers); 487 } 488 489 /** 490 * Determines whether a user attribute may be used as part of LTI membership 491 * @param array $toolconfig Tool config 492 * @param object $instanceconfig Tool instance config 493 * @param array $fields Set of fields to return if allowed or not 494 * @return array Verification which associates an attribute with a boolean (allowed or not) 495 */ 496 private static function is_allowed_field_set($toolconfig, $instanceconfig, $fields) { 497 $isallowedstate = []; 498 foreach ($fields as $key => $field) { 499 $allowed = isset($toolconfig[$field]) && (self::ALWAYS_INCLUDE_FIELD == $toolconfig[$field]); 500 if (!$allowed && isset($toolconfig[$field]) && (self::DELEGATE_TO_INSTRUCTOR == $toolconfig[$field]) && 501 !is_null($instanceconfig)) { 502 $allowed = isset($instanceconfig->{"lti_{$field}"}) && 503 ($instanceconfig->{"lti_{$field}"} == self::INSTRUCTOR_INCLUDED); 504 } 505 $isallowedstate[$key] = $allowed; 506 } 507 return $isallowedstate; 508 } 509 510 /** 511 * Adds form elements for membership add/edit page. 512 * 513 * @param \MoodleQuickForm $mform 514 */ 515 public function get_configuration_options(&$mform) { 516 $elementname = $this->get_component_id(); 517 $options = [ 518 get_string('notallow', $this->get_component_id()), 519 get_string('allow', $this->get_component_id()) 520 ]; 521 522 $mform->addElement('select', $elementname, get_string($elementname, $this->get_component_id()), $options); 523 $mform->setType($elementname, 'int'); 524 $mform->setDefault($elementname, 0); 525 $mform->addHelpButton($elementname, $elementname, $this->get_component_id()); 526 } 527 528 /** 529 * Return an array of key/values to add to the launch parameters. 530 * 531 * @param string $messagetype 'basic-lti-launch-request' or 'ContentItemSelectionRequest'. 532 * @param string $courseid The course id. 533 * @param string $user The user id. 534 * @param string $typeid The tool lti type id. 535 * @param string $modlti The id of the lti activity. 536 * 537 * The type is passed to check the configuration 538 * and not return parameters for services not used. 539 * 540 * @return array of key/value pairs to add as launch parameters. 541 */ 542 public function get_launch_parameters($messagetype, $courseid, $user, $typeid, $modlti = null) { 543 global $COURSE; 544 545 $launchparameters = array(); 546 $tool = lti_get_type_type_config($typeid); 547 if (isset($tool->{$this->get_component_id()})) { 548 if ($tool->{$this->get_component_id()} == parent::SERVICE_ENABLED && $this->is_used_in_context($typeid, $courseid)) { 549 $launchparameters['context_memberships_url'] = '$ToolProxyBinding.memberships.url'; 550 $launchparameters['context_memberships_v2_url'] = '$ToolProxyBinding.memberships.url'; 551 $launchparameters['context_memberships_versions'] = '1.0,2.0'; 552 } 553 } 554 return $launchparameters; 555 } 556 557 /** 558 * Return an array of key/claim mapping allowing LTI 1.1 custom parameters 559 * to be transformed to LTI 1.3 claims. 560 * 561 * @return array Key/value pairs of params to claim mapping. 562 */ 563 public function get_jwt_claim_mappings(): array { 564 return [ 565 'custom_context_memberships_v2_url' => [ 566 'suffix' => 'nrps', 567 'group' => 'namesroleservice', 568 'claim' => 'context_memberships_url', 569 'isarray' => false 570 ], 571 'custom_context_memberships_versions' => [ 572 'suffix' => 'nrps', 573 'group' => 'namesroleservice', 574 'claim' => 'service_versions', 575 'isarray' => true 576 ] 577 ]; 578 } 579 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body