See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
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 JSON for members. 107 * 108 * @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request 109 * @param \context_course $context Course context 110 * @param string $contextid Course ID 111 * @param object $tool Tool instance object 112 * @param string $role User role requested (empty if none) 113 * @param int $limitfrom Position of first record to be returned 114 * @param int $limitnum Maximum number of records to be returned 115 * @param object $lti LTI instance record 116 * @param \core_availability\info_module $info Conditional availability information 117 * for LTI instance (null if context-level request) 118 * 119 * @return string 120 * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. 121 * @see memberships::get_members_json($resource, $context, $course, $role, $limitfrom, $limitnum, $lti, $info, $response) 122 */ 123 public static function get_users_json($resource, $context, $contextid, $tool, $role, $limitfrom, $limitnum, $lti, $info) { 124 global $DB; 125 126 debugging('get_users_json() has been deprecated, ' . 127 'please use memberships::get_members_json() instead.', DEBUG_DEVELOPER); 128 129 $course = $DB->get_record('course', array('id' => $contextid), 'id,shortname,fullname', IGNORE_MISSING); 130 131 $memberships = new memberships(); 132 $memberships->check_tool($tool->id, null, array(self::SCOPE_MEMBERSHIPS_READ)); 133 134 $response = new \mod_lti\local\ltiservice\response(); 135 136 $json = $memberships->get_members_json($resource, $context, $course, $role, $limitfrom, $limitnum, $lti, $info, $response); 137 138 return $json; 139 } 140 141 /** 142 * Get the JSON for members. 143 * 144 * @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request 145 * @param \context_course $context Course context 146 * @param \course $course Course 147 * @param string $role User role requested (empty if none) 148 * @param int $limitfrom Position of first record to be returned 149 * @param int $limitnum Maximum number of records to be returned 150 * @param object $lti LTI instance record 151 * @param \core_availability\info_module $info Conditional availability information 152 * for LTI instance (null if context-level request) 153 * @param \mod_lti\local\ltiservice\response $response Response object for the request 154 * 155 * @return string 156 */ 157 public function get_members_json($resource, $context, $course, $role, $limitfrom, $limitnum, $lti, $info, $response) { 158 159 $withcapability = ''; 160 $exclude = array(); 161 if (!empty($role)) { 162 if ((strpos($role, 'http://') !== 0) && (strpos($role, 'https://') !== 0)) { 163 $role = self::CONTEXT_ROLE_PREFIX . $role; 164 } 165 if ($role === self::CONTEXT_ROLE_INSTRUCTOR) { 166 $withcapability = self::INSTRUCTOR_CAPABILITY; 167 } else if ($role === self::CONTEXT_ROLE_LEARNER) { 168 $exclude = array_keys(get_enrolled_users($context, self::INSTRUCTOR_CAPABILITY, 0, 'u.id', 169 null, null, null, true)); 170 } 171 } 172 $users = get_enrolled_users($context, $withcapability, 0, 'u.*', null, 0, 0, true); 173 if (($response->get_accept() === 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json') || 174 (($response->get_accept() !== 'application/vnd.ims.lis.v2.membershipcontainer+json') && 175 ($this->get_type()->ltiversion === LTI_VERSION_1P3))) { 176 $json = $this->users_to_json($resource, $users, $course, $exclude, $limitfrom, $limitnum, $lti, $info, $response); 177 } else { 178 $json = $this->users_to_jsonld($resource, $users, $course->id, $exclude, $limitfrom, $limitnum, $lti, $info, $response); 179 } 180 181 return $json; 182 } 183 184 /** 185 * Get the JSON-LD representation of the users. 186 * 187 * Note that when a limit is set and the exclude array is not empty, then the number of memberships 188 * returned may be less than the limit. 189 * 190 * @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request 191 * @param array $users Array of user records 192 * @param string $contextid Course ID 193 * @param array $exclude Array of user records to be excluded from the response 194 * @param int $limitfrom Position of first record to be returned 195 * @param int $limitnum Maximum number of records to be returned 196 * @param object $lti LTI instance record 197 * @param \core_availability\info_module $info Conditional availability information 198 * for LTI instance (null if context-level request) 199 * @param \mod_lti\local\ltiservice\response $response Response object for the request 200 * 201 * @return string 202 */ 203 private function users_to_jsonld($resource, $users, $contextid, $exclude, $limitfrom, $limitnum, 204 $lti, $info, $response) { 205 global $DB; 206 207 $tool = $this->get_type(); 208 $toolconfig = $this->get_typeconfig(); 209 $arrusers = [ 210 '@context' => 'http://purl.imsglobal.org/ctx/lis/v2/MembershipContainer', 211 '@type' => 'Page', 212 '@id' => $resource->get_endpoint(), 213 ]; 214 215 $arrusers['pageOf'] = [ 216 '@type' => 'LISMembershipContainer', 217 'membershipSubject' => [ 218 '@type' => 'Context', 219 'contextId' => $contextid, 220 'membership' => [] 221 ] 222 ]; 223 224 $enabledcapabilities = lti_get_enabled_capabilities($tool); 225 $islti2 = $tool->toolproxyid > 0; 226 $n = 0; 227 $more = false; 228 foreach ($users as $user) { 229 if (in_array($user->id, $exclude)) { 230 continue; 231 } 232 if (!empty($info) && !$info->is_user_visible($info->get_course_module(), $user->id)) { 233 continue; 234 } 235 $n++; 236 if ($limitnum > 0) { 237 if ($n <= $limitfrom) { 238 continue; 239 } 240 if (count($arrusers['pageOf']['membershipSubject']['membership']) >= $limitnum) { 241 $more = true; 242 break; 243 } 244 } 245 246 $member = new \stdClass(); 247 $member->{"@type" } = 'LISPerson'; 248 $membership = new \stdClass(); 249 $membership->status = 'Active'; 250 $membership->role = explode(',', lti_get_ims_role($user->id, null, $contextid, true)); 251 252 $instanceconfig = null; 253 if (!is_null($lti)) { 254 $instanceconfig = lti_get_type_config_from_instance($lti->id); 255 } 256 $isallowedlticonfig = self::is_allowed_field_set($toolconfig, $instanceconfig, 257 ['name' => 'sendname', 'email' => 'sendemailaddr']); 258 259 $includedcapabilities = [ 260 'User.id' => ['type' => 'id', 261 'member.field' => 'userId', 262 'source.value' => $user->id], 263 'Person.sourcedId' => ['type' => 'id', 264 'member.field' => 'sourcedId', 265 'source.value' => format_string($user->idnumber)], 266 'Person.name.full' => ['type' => 'name', 267 'member.field' => 'name', 268 'source.value' => format_string("{$user->firstname} {$user->lastname}")], 269 'Person.name.given' => ['type' => 'name', 270 'member.field' => 'givenName', 271 'source.value' => format_string($user->firstname)], 272 'Person.name.family' => ['type' => 'name', 273 'member.field' => 'familyName', 274 'source.value' => format_string($user->lastname)], 275 'Person.email.primary' => ['type' => 'email', 276 'member.field' => 'email', 277 'source.value' => format_string($user->email)], 278 'User.username' => ['type' => 'name', 279 'member.field' => 'ext_user_username', 280 'source.value' => format_string($user->username)] 281 ]; 282 283 if (!is_null($lti)) { 284 $message = new \stdClass(); 285 $message->message_type = 'basic-lti-launch-request'; 286 $conditions = array('courseid' => $contextid, 'itemtype' => 'mod', 287 'itemmodule' => 'lti', 'iteminstance' => $lti->id); 288 289 if (!empty($lti->servicesalt) && $DB->record_exists('grade_items', $conditions)) { 290 $message->lis_result_sourcedid = json_encode(lti_build_sourcedid($lti->id, 291 $user->id, 292 $lti->servicesalt, 293 $lti->typeid)); 294 // Not per specification but added to comply with earlier version of the service. 295 $member->resultSourcedId = $message->lis_result_sourcedid; 296 } 297 $membership->message = [$message]; 298 } 299 300 foreach ($includedcapabilities as $capabilityname => $capability) { 301 if ($islti2) { 302 if (in_array($capabilityname, $enabledcapabilities)) { 303 $member->{$capability['member.field']} = $capability['source.value']; 304 } 305 } else { 306 if (($capability['type'] === 'id') 307 || ($capability['type'] === 'name' && $isallowedlticonfig['name']) 308 || ($capability['type'] === 'email' && $isallowedlticonfig['email'])) { 309 $member->{$capability['member.field']} = $capability['source.value']; 310 } 311 } 312 } 313 314 $membership->member = $member; 315 316 $arrusers['pageOf']['membershipSubject']['membership'][] = $membership; 317 } 318 if ($more) { 319 $nextlimitfrom = $limitfrom + $limitnum; 320 $nextpage = "{$resource->get_endpoint()}?limit={$limitnum}&from={$nextlimitfrom}"; 321 if (!is_null($lti)) { 322 $nextpage .= "&rlid={$lti->id}"; 323 } 324 $arrusers['nextPage'] = $nextpage; 325 } 326 327 $response->set_content_type('application/vnd.ims.lis.v2.membershipcontainer+json'); 328 329 return json_encode($arrusers); 330 } 331 332 /** 333 * Get the NRP service JSON representation of the users. 334 * 335 * Note that when a limit is set and the exclude array is not empty, then the number of memberships 336 * returned may be less than the limit. 337 * 338 * @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request 339 * @param array $users Array of user records 340 * @param \course $course Course 341 * @param array $exclude Array of user records to be excluded from the response 342 * @param int $limitfrom Position of first record to be returned 343 * @param int $limitnum Maximum number of records to be returned 344 * @param object $lti LTI instance record 345 * @param \core_availability\info_module $info Conditional availability information for LTI instance 346 * @param \mod_lti\local\ltiservice\response $response Response object for the request 347 * 348 * @return string 349 */ 350 private function users_to_json($resource, $users, $course, $exclude, $limitfrom, $limitnum, 351 $lti, $info, $response) { 352 global $DB, $CFG; 353 354 $tool = $this->get_type(); 355 $toolconfig = $this->get_typeconfig(); 356 357 $context = new \stdClass(); 358 $context->id = $course->id; 359 $context->label = trim(html_to_text($course->shortname, 0)); 360 $context->title = trim(html_to_text($course->fullname, 0)); 361 362 $arrusers = [ 363 'id' => $resource->get_endpoint(), 364 'context' => $context, 365 'members' => [] 366 ]; 367 368 $islti2 = $tool->toolproxyid > 0; 369 $n = 0; 370 $more = false; 371 foreach ($users as $user) { 372 if (in_array($user->id, $exclude)) { 373 continue; 374 } 375 if (!empty($info) && !$info->is_user_visible($info->get_course_module(), $user->id)) { 376 continue; 377 } 378 $n++; 379 if ($limitnum > 0) { 380 if ($n <= $limitfrom) { 381 continue; 382 } 383 if (count($arrusers['members']) >= $limitnum) { 384 $more = true; 385 break; 386 } 387 } 388 389 $member = new \stdClass(); 390 $member->status = 'Active'; 391 $member->roles = explode(',', lti_get_ims_role($user->id, null, $course->id, true)); 392 393 $instanceconfig = null; 394 if (!is_null($lti)) { 395 $instanceconfig = lti_get_type_config_from_instance($lti->id); 396 } 397 if (!$islti2) { 398 $isallowedlticonfig = self::is_allowed_field_set($toolconfig, $instanceconfig, 399 ['name' => 'sendname', 'givenname' => 'sendname', 'familyname' => 'sendname', 400 'email' => 'sendemailaddr']); 401 } else { 402 $isallowedlticonfig = self::is_allowed_capability_set($tool, 403 ['name' => 'Person.name.full', 'givenname' => 'Person.name.given', 404 'familyname' => 'Person.name.family', 'email' => 'Person.email.primary']); 405 } 406 $includedcapabilities = [ 407 'User.id' => ['type' => 'id', 408 'member.field' => 'user_id', 409 'source.value' => $user->id], 410 'Person.sourcedId' => ['type' => 'id', 411 'member.field' => 'lis_person_sourcedid', 412 'source.value' => format_string($user->idnumber)], 413 'Person.name.full' => ['type' => 'name', 414 'member.field' => 'name', 415 'source.value' => format_string("{$user->firstname} {$user->lastname}")], 416 'Person.name.given' => ['type' => 'givenname', 417 'member.field' => 'given_name', 418 'source.value' => format_string($user->firstname)], 419 'Person.name.family' => ['type' => 'familyname', 420 'member.field' => 'family_name', 421 'source.value' => format_string($user->lastname)], 422 'Person.email.primary' => ['type' => 'email', 423 'member.field' => 'email', 424 'source.value' => format_string($user->email)] 425 ]; 426 427 if (!is_null($lti)) { 428 $message = new \stdClass(); 429 $message->{'https://purl.imsglobal.org/spec/lti/claim/message_type'} = 'LtiResourceLinkRequest'; 430 $conditions = array('courseid' => $course->id, 'itemtype' => 'mod', 431 'itemmodule' => 'lti', 'iteminstance' => $lti->id); 432 433 if (!empty($lti->servicesalt) && $DB->record_exists('grade_items', $conditions)) { 434 $basicoutcome = new \stdClass(); 435 $basicoutcome->lis_result_sourcedid = json_encode(lti_build_sourcedid($lti->id, 436 $user->id, 437 $lti->servicesalt, 438 $lti->typeid)); 439 // Add outcome service URL. 440 $serviceurl = new \moodle_url('/mod/lti/service.php'); 441 $serviceurl = $serviceurl->out(); 442 $forcessl = false; 443 if (!empty($CFG->mod_lti_forcessl)) { 444 $forcessl = true; 445 } 446 if ((isset($toolconfig['forcessl']) && ($toolconfig['forcessl'] == '1')) or $forcessl) { 447 $serviceurl = lti_ensure_url_is_https($serviceurl); 448 } 449 $basicoutcome->lis_outcome_service_url = $serviceurl; 450 $message->{'https://purl.imsglobal.org/spec/lti-bo/claim/basicoutcome'} = $basicoutcome; 451 } 452 $member->message = [$message]; 453 } 454 455 foreach ($includedcapabilities as $capabilityname => $capability) { 456 if (($capability['type'] === 'id') || $isallowedlticonfig[$capability['type']]) { 457 $member->{$capability['member.field']} = $capability['source.value']; 458 } 459 } 460 461 $arrusers['members'][] = $member; 462 } 463 if ($more) { 464 $nextlimitfrom = $limitfrom + $limitnum; 465 $nextpage = "{$resource->get_endpoint()}?limit={$limitnum}&from={$nextlimitfrom}"; 466 if (!is_null($lti)) { 467 $nextpage .= "&rlid={$lti->id}"; 468 } 469 $response->add_additional_header("Link: <{$nextpage}>; rel=\"next\""); 470 } 471 472 $response->set_content_type('application/vnd.ims.lti-nrps.v2.membershipcontainer+json'); 473 474 return json_encode($arrusers); 475 } 476 477 /** 478 * Determines whether a user attribute may be used as part of LTI membership 479 * @param array $toolconfig Tool config 480 * @param object $instanceconfig Tool instance config 481 * @param array $fields Set of fields to return if allowed or not 482 * @return array Verification which associates an attribute with a boolean (allowed or not) 483 */ 484 private static function is_allowed_field_set($toolconfig, $instanceconfig, $fields) { 485 $isallowedstate = []; 486 foreach ($fields as $key => $field) { 487 $allowed = isset($toolconfig[$field]) && (self::ALWAYS_INCLUDE_FIELD == $toolconfig[$field]); 488 if (!$allowed && isset($toolconfig[$field]) && (self::DELEGATE_TO_INSTRUCTOR == $toolconfig[$field]) && 489 !is_null($instanceconfig)) { 490 $allowed = isset($instanceconfig->{"lti_{$field}"}) && 491 ($instanceconfig->{"lti_{$field}"} == self::INSTRUCTOR_INCLUDED); 492 } 493 $isallowedstate[$key] = $allowed; 494 } 495 return $isallowedstate; 496 } 497 498 /** 499 * Adds form elements for membership add/edit page. 500 * 501 * @param \MoodleQuickForm $mform 502 */ 503 public function get_configuration_options(&$mform) { 504 $elementname = $this->get_component_id(); 505 $options = [ 506 get_string('notallow', $this->get_component_id()), 507 get_string('allow', $this->get_component_id()) 508 ]; 509 510 $mform->addElement('select', $elementname, get_string($elementname, $this->get_component_id()), $options); 511 $mform->setType($elementname, 'int'); 512 $mform->setDefault($elementname, 0); 513 $mform->addHelpButton($elementname, $elementname, $this->get_component_id()); 514 } 515 516 /** 517 * Return an array of key/values to add to the launch parameters. 518 * 519 * @param string $messagetype 'basic-lti-launch-request' or 'ContentItemSelectionRequest'. 520 * @param string $courseid The course id. 521 * @param string $user The user id. 522 * @param string $typeid The tool lti type id. 523 * @param string $modlti The id of the lti activity. 524 * 525 * The type is passed to check the configuration 526 * and not return parameters for services not used. 527 * 528 * @return array of key/value pairs to add as launch parameters. 529 */ 530 public function get_launch_parameters($messagetype, $courseid, $user, $typeid, $modlti = null) { 531 global $COURSE; 532 533 $launchparameters = array(); 534 $tool = lti_get_type_type_config($typeid); 535 if (isset($tool->{$this->get_component_id()})) { 536 if ($tool->{$this->get_component_id()} == parent::SERVICE_ENABLED && $this->is_used_in_context($typeid, $courseid)) { 537 $launchparameters['context_memberships_url'] = '$ToolProxyBinding.memberships.url'; 538 $launchparameters['context_memberships_versions'] = '1.0,2.0'; 539 } 540 } 541 return $launchparameters; 542 } 543 544 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body