[ Index ] |
PHP Cross Reference of moodle-2.8 |
[Summary view] [Print] [Text view]
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 // This file is part of BasicLTI4Moodle 18 // 19 // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) 20 // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web 21 // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI 22 // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS 23 // are already supporting or going to support BasicLTI. This project Implements the consumer 24 // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. 25 // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem 26 // at the GESSI research group at UPC. 27 // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI 28 // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a 29 // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. 30 // 31 // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis 32 // of the Universitat Politecnica de Catalunya http://www.upc.edu 33 // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. 34 35 /** 36 * This file contains the library of functions and constants for the lti module 37 * 38 * @package mod_lti 39 * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis 40 * [email protected] 41 * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu 42 * @author Marc Alier 43 * @author Jordi Piguillem 44 * @author Nikolas Galanis 45 * @author Chris Scribner 46 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 47 */ 48 49 defined('MOODLE_INTERNAL') || die; 50 51 // TODO: Switch to core oauthlib once implemented - MDL-30149. 52 use moodle\mod\lti as lti; 53 54 require_once($CFG->dirroot.'/mod/lti/OAuth.php'); 55 56 define('LTI_URL_DOMAIN_REGEX', '/(?:https?:\/\/)?(?:www\.)?([^\/]+)(?:\/|$)/i'); 57 58 define('LTI_LAUNCH_CONTAINER_DEFAULT', 1); 59 define('LTI_LAUNCH_CONTAINER_EMBED', 2); 60 define('LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS', 3); 61 define('LTI_LAUNCH_CONTAINER_WINDOW', 4); 62 define('LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW', 5); 63 64 define('LTI_TOOL_STATE_ANY', 0); 65 define('LTI_TOOL_STATE_CONFIGURED', 1); 66 define('LTI_TOOL_STATE_PENDING', 2); 67 define('LTI_TOOL_STATE_REJECTED', 3); 68 define('LTI_TOOL_PROXY_TAB', 4); 69 70 define('LTI_TOOL_PROXY_STATE_CONFIGURED', 1); 71 define('LTI_TOOL_PROXY_STATE_PENDING', 2); 72 define('LTI_TOOL_PROXY_STATE_ACCEPTED', 3); 73 define('LTI_TOOL_PROXY_STATE_REJECTED', 4); 74 75 define('LTI_SETTING_NEVER', 0); 76 define('LTI_SETTING_ALWAYS', 1); 77 define('LTI_SETTING_DELEGATE', 2); 78 79 /** 80 * Prints a Basic LTI activity 81 * 82 * $param int $basicltiid Basic LTI activity id 83 */ 84 function lti_view($instance) { 85 global $PAGE, $CFG; 86 87 if (empty($instance->typeid)) { 88 $tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course); 89 if ($tool) { 90 $typeid = $tool->id; 91 } else { 92 $typeid = null; 93 } 94 } else { 95 $typeid = $instance->typeid; 96 $tool = lti_get_type($typeid); 97 } 98 99 if ($typeid) { 100 $typeconfig = lti_get_type_config($typeid); 101 } else { 102 // There is no admin configuration for this tool. Use configuration in the lti instance record plus some defaults. 103 $typeconfig = (array)$instance; 104 105 $typeconfig['sendname'] = $instance->instructorchoicesendname; 106 $typeconfig['sendemailaddr'] = $instance->instructorchoicesendemailaddr; 107 $typeconfig['customparameters'] = $instance->instructorcustomparameters; 108 $typeconfig['acceptgrades'] = $instance->instructorchoiceacceptgrades; 109 $typeconfig['allowroster'] = $instance->instructorchoiceallowroster; 110 $typeconfig['forcessl'] = '0'; 111 } 112 113 // Default the organizationid if not specified. 114 if (empty($typeconfig['organizationid'])) { 115 $urlparts = parse_url($CFG->wwwroot); 116 117 $typeconfig['organizationid'] = $urlparts['host']; 118 } 119 120 if (isset($tool->toolproxyid)) { 121 $toolproxy = lti_get_tool_proxy($tool->toolproxyid); 122 $key = $toolproxy->guid; 123 $secret = $toolproxy->secret; 124 } else { 125 $toolproxy = null; 126 if (!empty($instance->resourcekey)) { 127 $key = $instance->resourcekey; 128 } else if (!empty($typeconfig['resourcekey'])) { 129 $key = $typeconfig['resourcekey']; 130 } else { 131 $key = ''; 132 } 133 if (!empty($instance->password)) { 134 $secret = $instance->password; 135 } else if (!empty($typeconfig['password'])) { 136 $secret = $typeconfig['password']; 137 } else { 138 $secret = ''; 139 } 140 } 141 142 $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $typeconfig['toolurl']; 143 $endpoint = trim($endpoint); 144 145 // If the current request is using SSL and a secure tool URL is specified, use it. 146 if (lti_request_is_using_ssl() && !empty($instance->securetoolurl)) { 147 $endpoint = trim($instance->securetoolurl); 148 } 149 150 // If SSL is forced, use the secure tool url if specified. Otherwise, make sure https is on the normal launch URL. 151 if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) { 152 if (!empty($instance->securetoolurl)) { 153 $endpoint = trim($instance->securetoolurl); 154 } 155 156 $endpoint = lti_ensure_url_is_https($endpoint); 157 } else { 158 if (!strstr($endpoint, '://')) { 159 $endpoint = 'http://' . $endpoint; 160 } 161 } 162 163 $orgid = $typeconfig['organizationid']; 164 165 $course = $PAGE->course; 166 $islti2 = isset($tool->toolproxyid); 167 $allparams = lti_build_request($instance, $typeconfig, $course, $typeid, $islti2); 168 if ($islti2) { 169 $requestparams = lti_build_request_lti2($tool, $allparams); 170 } else { 171 $requestparams = $allparams; 172 } 173 $requestparams = array_merge($requestparams, lti_build_standard_request($instance, $orgid, $islti2)); 174 $customstr = ''; 175 if (isset($typeconfig['customparameters'])) { 176 $customstr = $typeconfig['customparameters']; 177 } 178 $requestparams = array_merge($requestparams, lti_build_custom_parameters($toolproxy, $tool, $instance, $allparams, $customstr, 179 $instance->instructorcustomparameters, $islti2)); 180 181 $launchcontainer = lti_get_launch_container($instance, $typeconfig); 182 $returnurlparams = array('course' => $course->id, 183 'launch_container' => $launchcontainer, 184 'instanceid' => $instance->id, 185 'sesskey' => sesskey()); 186 187 // Add the return URL. We send the launch container along to help us avoid frames-within-frames when the user returns. 188 $url = new \moodle_url('/mod/lti/return.php', $returnurlparams); 189 $returnurl = $url->out(false); 190 191 if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) { 192 $returnurl = lti_ensure_url_is_https($returnurl); 193 } 194 195 $target = ''; 196 switch($launchcontainer) { 197 case LTI_LAUNCH_CONTAINER_EMBED: 198 case LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS: 199 $target = 'iframe'; 200 break; 201 case LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW: 202 $target = 'frame'; 203 break; 204 case LTI_LAUNCH_CONTAINER_WINDOW: 205 $target = 'window'; 206 break; 207 } 208 if (!empty($target)) { 209 $requestparams['launch_presentation_document_target'] = $target; 210 } 211 212 $requestparams['launch_presentation_return_url'] = $returnurl; 213 214 // Allow request params to be updated by sub-plugins. 215 $plugins = core_component::get_plugin_list('ltisource'); 216 foreach (array_keys($plugins) as $plugin) { 217 $pluginparams = component_callback('ltisource_'.$plugin, 'before_launch', 218 array($instance, $endpoint, $requestparams), array()); 219 220 if (!empty($pluginparams) && is_array($pluginparams)) { 221 $requestparams = array_merge($requestparams, $pluginparams); 222 } 223 } 224 225 if (!empty($key) && !empty($secret)) { 226 $parms = lti_sign_parameters($requestparams, $endpoint, "POST", $key, $secret); 227 228 $endpointurl = new \moodle_url($endpoint); 229 $endpointparams = $endpointurl->params(); 230 231 // Strip querystring params in endpoint url from $parms to avoid duplication. 232 if (!empty($endpointparams) && !empty($parms)) { 233 foreach (array_keys($endpointparams) as $paramname) { 234 if (isset($parms[$paramname])) { 235 unset($parms[$paramname]); 236 } 237 } 238 } 239 240 } else { 241 // If no key and secret, do the launch unsigned. 242 $returnurlparams['unsigned'] = '1'; 243 $parms = $requestparams; 244 } 245 246 $debuglaunch = ( $instance->debuglaunch == 1 ); 247 248 $content = lti_post_launch_html($parms, $endpoint, $debuglaunch); 249 250 echo $content; 251 } 252 253 /** 254 * Prepares an LTI registration request message 255 * 256 * $param object $instance Tool Proxy instance object 257 */ 258 function lti_register($toolproxy) { 259 global $PAGE, $CFG; 260 261 $key = $toolproxy->guid; 262 $secret = $toolproxy->secret; 263 $endpoint = $toolproxy->regurl; 264 265 $requestparams = array(); 266 $requestparams['lti_message_type'] = 'ToolProxyRegistrationRequest'; 267 $requestparams['lti_version'] = 'LTI-2p0'; 268 $requestparams['reg_key'] = $key; 269 $requestparams['reg_password'] = $secret; 270 271 // Change the status to pending. 272 $toolproxy->state = LTI_TOOL_PROXY_STATE_PENDING; 273 lti_update_tool_proxy($toolproxy); 274 275 // Add the profile URL. 276 $profileservice = lti_get_service_by_name('profile'); 277 $profileservice->set_tool_proxy($toolproxy); 278 $requestparams['tc_profile_url'] = $profileservice->parse_value('$ToolConsumerProfile.url'); 279 280 // Add the return URL. 281 $returnurlparams = array('id' => $toolproxy->id, 'sesskey'=>sesskey()); 282 $url = new \moodle_url('/mod/lti/registrationreturn.php', $returnurlparams); 283 $returnurl = $url->out(false); 284 285 $requestparams['launch_presentation_return_url'] = $returnurl; 286 $content = lti_post_launch_html($requestparams, $endpoint, false); 287 288 echo $content; 289 } 290 291 /** 292 * Build source ID 293 * 294 * @param int $instanceid 295 * @param int $userid 296 * @param string $servicesalt 297 * @param null|int $typeid 298 * @param null|int $launchid 299 * @return stdClass 300 */ 301 function lti_build_sourcedid($instanceid, $userid, $servicesalt, $typeid = null, $launchid = null) { 302 $data = new \stdClass(); 303 304 $data->instanceid = $instanceid; 305 $data->userid = $userid; 306 $data->typeid = $typeid; 307 if (!empty($launchid)) { 308 $data->launchid = $launchid; 309 } else { 310 $data->launchid = mt_rand(); 311 } 312 313 $json = json_encode($data); 314 315 $hash = hash('sha256', $json . $servicesalt, false); 316 317 $container = new \stdClass(); 318 $container->data = $data; 319 $container->hash = $hash; 320 321 return $container; 322 } 323 324 /** 325 * This function builds the request that must be sent to the tool producer 326 * 327 * @param object $instance Basic LTI instance object 328 * @param array $typeconfig Basic LTI tool configuration 329 * @param object $course Course object 330 * @param int|null $typeid Basic LTI tool ID 331 * @param boolean $islti2 True if an LTI 2 tool is being launched 332 * 333 * @return array Request details 334 */ 335 function lti_build_request($instance, $typeconfig, $course, $typeid = null, $islti2 = false) { 336 global $USER, $CFG; 337 338 if (empty($instance->cmid)) { 339 $instance->cmid = 0; 340 } 341 342 $role = lti_get_ims_role($USER, $instance->cmid, $instance->course, $islti2); 343 344 $intro = ''; 345 if (!empty($instance->cmid)) { 346 $intro = format_module_intro('lti', $instance, $instance->cmid); 347 $intro = html_to_text($intro, 0, false); 348 349 // This may look weird, but this is required for new lines 350 // so we generate the same OAuth signature as the tool provider. 351 $intro = str_replace("\n", "\r\n", $intro); 352 } 353 $requestparams = array( 354 'resource_link_title' => $instance->name, 355 'resource_link_description' => $intro, 356 'user_id' => $USER->id, 357 'lis_person_sourcedid' => $USER->idnumber, 358 'roles' => $role, 359 'context_id' => $course->id, 360 'context_label' => $course->shortname, 361 'context_title' => $course->fullname, 362 ); 363 if ($course->format == 'site') { 364 $requestparams['context_type'] = 'Group'; 365 } else { 366 $requestparams['context_type'] = 'CourseSection'; 367 $requestparams['lis_course_section_sourcedid'] = $course->idnumber; 368 } 369 $placementsecret = $instance->servicesalt; 370 371 if ( isset($placementsecret) ) { 372 $sourcedid = json_encode(lti_build_sourcedid($instance->id, $USER->id, $placementsecret, $typeid)); 373 $requestparams['lis_result_sourcedid'] = $sourcedid; 374 } 375 376 if ( isset($placementsecret) && ($islti2 || 377 $typeconfig['acceptgrades'] == LTI_SETTING_ALWAYS || 378 ($typeconfig['acceptgrades'] == LTI_SETTING_DELEGATE && $instance->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS))) { 379 380 // Add outcome service URL. 381 $serviceurl = new \moodle_url('/mod/lti/service.php'); 382 $serviceurl = $serviceurl->out(); 383 384 $forcessl = false; 385 if (!empty($CFG->mod_lti_forcessl)) { 386 $forcessl = true; 387 } 388 389 if ((isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) or $forcessl) { 390 $serviceurl = lti_ensure_url_is_https($serviceurl); 391 } 392 393 $requestparams['lis_outcome_service_url'] = $serviceurl; 394 } 395 396 // Send user's name and email data if appropriate. 397 if ($islti2 || $typeconfig['sendname'] == LTI_SETTING_ALWAYS || 398 ( $typeconfig['sendname'] == LTI_SETTING_DELEGATE && $instance->instructorchoicesendname == LTI_SETTING_ALWAYS ) ) { 399 $requestparams['lis_person_name_given'] = $USER->firstname; 400 $requestparams['lis_person_name_family'] = $USER->lastname; 401 $requestparams['lis_person_name_full'] = $USER->firstname . ' ' . $USER->lastname; 402 $requestparams['ext_user_username'] = $USER->username; 403 } 404 405 if ($islti2 || $typeconfig['sendemailaddr'] == LTI_SETTING_ALWAYS || 406 ($typeconfig['sendemailaddr'] == LTI_SETTING_DELEGATE && $instance->instructorchoicesendemailaddr == LTI_SETTING_ALWAYS)) { 407 $requestparams['lis_person_contact_email_primary'] = $USER->email; 408 } 409 410 return $requestparams; 411 } 412 413 /** 414 * This function builds the request that must be sent to an LTI 2 tool provider 415 * 416 * @param object $tool Basic LTI tool object 417 * @param array $params Custom launch parameters 418 * 419 * @return array Request details 420 */ 421 function lti_build_request_lti2($tool, $params) { 422 423 $requestparams = array(); 424 425 $capabilities = lti_get_capabilities(); 426 $enabledcapabilities = explode("\n", $tool->enabledcapability); 427 foreach ($enabledcapabilities as $capability) { 428 if (array_key_exists($capability, $capabilities)) { 429 $val = $capabilities[$capability]; 430 if ($val && (substr($val, 0, 1) != '$')) { 431 if (isset($params[$val])) { 432 $requestparams[$capabilities[$capability]] = $params[$capabilities[$capability]]; 433 } 434 } 435 } 436 } 437 438 return $requestparams; 439 440 } 441 442 /** 443 * This function builds the standard parameters for an LTI 1 or 2 request that must be sent to the tool producer 444 * 445 * @param object $instance Basic LTI instance object 446 * @param string $orgid Organisation ID 447 * @param boolean $islti2 True if an LTI 2 tool is being launched 448 * 449 * @return array Request details 450 */ 451 function lti_build_standard_request($instance, $orgid, $islti2) { 452 global $CFG; 453 454 $requestparams = array(); 455 456 $requestparams['resource_link_id'] = $instance->id; 457 if (property_exists($instance, 'resource_link_id') and !empty($instance->resource_link_id)) { 458 $requestparams['resource_link_id'] = $instance->resource_link_id; 459 } 460 461 $requestparams['launch_presentation_locale'] = current_language(); 462 463 // Make sure we let the tool know what LMS they are being called from. 464 $requestparams['ext_lms'] = 'moodle-2'; 465 $requestparams['tool_consumer_info_product_family_code'] = 'moodle'; 466 $requestparams['tool_consumer_info_version'] = strval($CFG->version); 467 468 // Add oauth_callback to be compliant with the 1.0A spec. 469 $requestparams['oauth_callback'] = 'about:blank'; 470 471 if (!$islti2) { 472 $requestparams['lti_version'] = 'LTI-1p0'; 473 } else { 474 $requestparams['lti_version'] = 'LTI-2p0'; 475 } 476 $requestparams['lti_message_type'] = 'basic-lti-launch-request'; 477 478 if ($orgid) { 479 $requestparams["tool_consumer_instance_guid"] = $orgid; 480 } 481 if (!empty($CFG->mod_lti_institution_name)) { 482 $requestparams['tool_consumer_instance_name'] = $CFG->mod_lti_institution_name; 483 } else { 484 $requestparams['tool_consumer_instance_name'] = get_site()->fullname; 485 } 486 487 return $requestparams; 488 } 489 490 /** 491 * This function builds the custom parameters 492 * 493 * @param object $toolproxy Tool proxy instance object 494 * @param object $tool Tool instance object 495 * @param object $instance Tool placement instance object 496 * @param array $params LTI launch parameters 497 * @param string $customstr Custom parameters defined for tool 498 * @param string $instructorcustomstr Custom parameters defined for this placement 499 * @param boolean $islti2 True if an LTI 2 tool is being launched 500 * 501 * @return array Custom parameters 502 */ 503 function lti_build_custom_parameters($toolproxy, $tool, $instance, $params, $customstr, $instructorcustomstr, $islti2) { 504 505 // Concatenate the custom parameters from the administrator and the instructor 506 // Instructor parameters are only taken into consideration if the administrator 507 // has given permission. 508 $custom = array(); 509 if ($customstr) { 510 $custom = lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2); 511 } 512 if (!isset($typeconfig['allowinstructorcustom']) || $typeconfig['allowinstructorcustom'] != LTI_SETTING_NEVER) { 513 if ($instructorcustomstr) { 514 $custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params, $instructorcustomstr, $islti2), $custom); 515 } 516 } 517 if ($islti2) { 518 $custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params, $tool->parameter, true), $custom); 519 $settings = lti_get_tool_settings($tool->toolproxyid); 520 $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings)); 521 $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course); 522 $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings)); 523 $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course, $instance->id); 524 $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings)); 525 } 526 527 return $custom; 528 } 529 530 function lti_get_tool_table($tools, $id) { 531 global $CFG, $OUTPUT, $USER; 532 $html = ''; 533 534 $typename = get_string('typename', 'lti'); 535 $baseurl = get_string('baseurl', 'lti'); 536 $action = get_string('action', 'lti'); 537 $createdon = get_string('createdon', 'lti'); 538 539 if (!empty($tools)) { 540 $html .= " 541 <div id=\"{$id}_tools_container\" style=\"margin-top:.5em;margin-bottom:.5em\"> 542 <table id=\"{$id}_tools\"> 543 <thead> 544 <tr> 545 <th>$typename</th> 546 <th>$baseurl</th> 547 <th>$createdon</th> 548 <th>$action</th> 549 </tr> 550 </thead> 551 "; 552 553 foreach ($tools as $type) { 554 $date = userdate($type->timecreated, get_string('strftimedatefullshort', 'core_langconfig')); 555 $accept = get_string('accept', 'lti'); 556 $update = get_string('update', 'lti'); 557 $delete = get_string('delete', 'lti'); 558 559 if (empty($type->toolproxyid)) { 560 $baseurl = new \moodle_url('/mod/lti/typessettings.php', array( 561 'action' => 'accept', 562 'id' => $type->id, 563 'sesskey' => sesskey(), 564 'tab' => $id 565 )); 566 $ref = $type->baseurl; 567 } else { 568 $baseurl = new \moodle_url('/mod/lti/toolssettings.php', array( 569 'action' => 'accept', 570 'id' => $type->id, 571 'sesskey' => sesskey(), 572 'tab' => $id 573 )); 574 $ref = $type->tpname; 575 } 576 577 $accepthtml = $OUTPUT->action_icon($baseurl, 578 new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null, 579 array('title' => $accept, 'class' => 'editing_accept')); 580 581 $deleteaction = 'delete'; 582 583 if ($type->state == LTI_TOOL_STATE_CONFIGURED) { 584 $accepthtml = ''; 585 } 586 587 if ($type->state != LTI_TOOL_STATE_REJECTED) { 588 $deleteaction = 'reject'; 589 $delete = get_string('reject', 'lti'); 590 } 591 592 $updateurl = clone($baseurl); 593 $updateurl->param('action', 'update'); 594 $updatehtml = $OUTPUT->action_icon($updateurl, 595 new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null, 596 array('title' => $update, 'class' => 'editing_update')); 597 598 if (($type->state != LTI_TOOL_STATE_REJECTED) || empty($type->toolproxyid)) { 599 $deleteurl = clone($baseurl); 600 $deleteurl->param('action', $deleteaction); 601 $deletehtml = $OUTPUT->action_icon($deleteurl, 602 new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null, 603 array('title' => $delete, 'class' => 'editing_delete')); 604 } else { 605 $deletehtml = ''; 606 } 607 $html .= " 608 <tr> 609 <td> 610 {$type->name} 611 </td> 612 <td> 613 {$ref} 614 </td> 615 <td> 616 {$date} 617 </td> 618 <td align=\"center\"> 619 {$accepthtml}{$updatehtml}{$deletehtml} 620 </td> 621 </tr> 622 "; 623 } 624 $html .= '</table></div>'; 625 } else { 626 $html .= get_string('no_' . $id, 'lti'); 627 } 628 629 return $html; 630 } 631 632 /** 633 * This function builds the tab for a category of tool proxies 634 * 635 * @param object $toolproxies Tool proxy instance objects 636 * @param string $id Category ID 637 * 638 * @return string HTML for tab 639 */ 640 function lti_get_tool_proxy_table($toolproxies, $id) { 641 global $OUTPUT; 642 643 if (!empty($toolproxies)) { 644 $typename = get_string('typename', 'lti'); 645 $url = get_string('registrationurl', 'lti'); 646 $action = get_string('action', 'lti'); 647 $createdon = get_string('createdon', 'lti'); 648 649 $html = <<< EOD 650 <div id="{$id}_tool_proxies_container" style="margin-top: 0.5em; margin-bottom: 0.5em"> 651 <table id="{$id}_tool_proxies"> 652 <thead> 653 <tr> 654 <th>{$typename}</th> 655 <th>{$url}</th> 656 <th>{$createdon}</th> 657 <th>{$action}</th> 658 </tr> 659 </thead> 660 EOD; 661 foreach ($toolproxies as $toolproxy) { 662 $date = userdate($toolproxy->timecreated, get_string('strftimedatefullshort', 'core_langconfig')); 663 $accept = get_string('register', 'lti'); 664 $update = get_string('update', 'lti'); 665 $delete = get_string('delete', 'lti'); 666 667 $baseurl = new \moodle_url('/mod/lti/registersettings.php', array( 668 'action' => 'accept', 669 'id' => $toolproxy->id, 670 'sesskey' => sesskey(), 671 'tab' => $id 672 )); 673 674 $registerurl = new \moodle_url('/mod/lti/register.php', array( 675 'id' => $toolproxy->id, 676 'sesskey' => sesskey(), 677 'tab' => 'tool_proxy' 678 )); 679 680 $accepthtml = $OUTPUT->action_icon($registerurl, 681 new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null, 682 array('title' => $accept, 'class' => 'editing_accept')); 683 684 $deleteaction = 'delete'; 685 686 if ($toolproxy->state != LTI_TOOL_PROXY_STATE_CONFIGURED) { 687 $accepthtml = ''; 688 } 689 690 if (($toolproxy->state == LTI_TOOL_PROXY_STATE_CONFIGURED) || ($toolproxy->state == LTI_TOOL_PROXY_STATE_PENDING)) { 691 $delete = get_string('cancel', 'lti'); 692 } 693 694 $updateurl = clone($baseurl); 695 $updateurl->param('action', 'update'); 696 $updatehtml = $OUTPUT->action_icon($updateurl, 697 new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null, 698 array('title' => $update, 'class' => 'editing_update')); 699 700 $deleteurl = clone($baseurl); 701 $deleteurl->param('action', $deleteaction); 702 $deletehtml = $OUTPUT->action_icon($deleteurl, 703 new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null, 704 array('title' => $delete, 'class' => 'editing_delete')); 705 $html .= <<< EOD 706 <tr> 707 <td> 708 {$toolproxy->name} 709 </td> 710 <td> 711 {$toolproxy->regurl} 712 </td> 713 <td> 714 {$date} 715 </td> 716 <td align="center"> 717 {$accepthtml}{$updatehtml}{$deletehtml} 718 </td> 719 </tr> 720 EOD; 721 } 722 $html .= '</table></div>'; 723 } else { 724 $html = get_string('no_' . $id, 'lti'); 725 } 726 727 return $html; 728 } 729 730 /** 731 * Splits the custom parameters field to the various parameters 732 * 733 * @param object $toolproxy Tool proxy instance object 734 * @param object $tool Tool instance object 735 * @param array $params LTI launch parameters 736 * @param string $customstr String containing the parameters 737 * @param boolean $islti2 True if an LTI 2 tool is being launched 738 * 739 * @return Array of custom parameters 740 */ 741 function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2 = false) { 742 $customstr = str_replace("\r\n", "\n", $customstr); 743 $customstr = str_replace("\n\r", "\n", $customstr); 744 $customstr = str_replace("\r", "\n", $customstr); 745 $lines = explode("\n", $customstr); // Or should this split on "/[\n;]/"? 746 $retval = array(); 747 foreach ($lines as $line) { 748 $pos = strpos($line, "="); 749 if ( $pos === false || $pos < 1 ) { 750 continue; 751 } 752 $key = trim(core_text::substr($line, 0, $pos)); 753 $val = trim(core_text::substr($line, $pos + 1, strlen($line))); 754 $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, $islti2); 755 $key2 = lti_map_keyname($key); 756 $retval['custom_'.$key2] = $val; 757 if ($islti2 && ($key != $key2)) { 758 $retval['custom_'.$key] = $val; 759 } 760 } 761 return $retval; 762 } 763 764 /** 765 * Adds the custom parameters to an array 766 * 767 * @param object $toolproxy Tool proxy instance object 768 * @param object $tool Tool instance object 769 * @param array $params LTI launch parameters 770 * @param array $parameters Array containing the parameters 771 * 772 * @return array Array of custom parameters 773 */ 774 function lti_get_custom_parameters($toolproxy, $tool, $params, $parameters) { 775 $retval = array(); 776 foreach ($parameters as $key => $val) { 777 $key2 = lti_map_keyname($key); 778 $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, true); 779 $retval['custom_'.$key2] = $val; 780 if ($key != $key2) { 781 $retval['custom_'.$key] = $val; 782 } 783 } 784 return $retval; 785 } 786 787 /** 788 * Parse a custom parameter to replace any substitution variables 789 * 790 * @param object $toolproxy Tool proxy instance object 791 * @param object $tool Tool instance object 792 * @param array $params LTI launch parameters 793 * @param string $value Custom parameter value 794 * @param boolean $islti2 True if an LTI 2 tool is being launched 795 * 796 * @return Parsed value of custom parameter 797 */ 798 function lti_parse_custom_parameter($toolproxy, $tool, $params, $value, $islti2) { 799 global $USER, $COURSE; 800 801 if ($value) { 802 if (substr($value, 0, 1) == '\\') { 803 $value = substr($value, 1); 804 } else if (substr($value, 0, 1) == '$') { 805 $value1 = substr($value, 1); 806 $enabledcapabilities = explode("\n", $tool->enabledcapability); 807 if (!$islti2 || in_array($value1, $enabledcapabilities)) { 808 $capabilities = lti_get_capabilities(); 809 if (array_key_exists($value1, $capabilities)) { 810 $val = $capabilities[$value1]; 811 if ($val) { 812 if (substr($val, 0, 1) != '$') { 813 $value = $params[$val]; 814 } else { 815 $valarr = explode('->', substr($val, 1), 2); 816 $value = "{${$valarr[0]}->$valarr[1]}"; 817 $value = str_replace('<br />' , ' ', $value); 818 $value = str_replace('<br>' , ' ', $value); 819 $value = format_string($value); 820 } 821 } 822 } else if ($islti2) { 823 $val = $value; 824 $services = lti_get_services(); 825 foreach ($services as $service) { 826 $service->set_tool_proxy($toolproxy); 827 $value = $service->parse_value($val); 828 if ($val != $value) { 829 break; 830 } 831 } 832 } 833 } 834 } 835 } 836 return $value; 837 } 838 839 /** 840 * Used for building the names of the different custom parameters 841 * 842 * @param string $key Parameter name 843 * 844 * @return string Processed name 845 */ 846 function lti_map_keyname($key) { 847 $newkey = ""; 848 $key = core_text::strtolower(trim($key)); 849 foreach (str_split($key) as $ch) { 850 if ( ($ch >= 'a' && $ch <= 'z') || ($ch >= '0' && $ch <= '9') ) { 851 $newkey .= $ch; 852 } else { 853 $newkey .= '_'; 854 } 855 } 856 return $newkey; 857 } 858 859 /** 860 * Gets the IMS role string for the specified user and LTI course module. 861 * 862 * @param mixed $user User object or user id 863 * @param int $cmid The course module id of the LTI activity 864 * @param int $courseid The course id of the LTI activity 865 * @param boolean $islti2 True if an LTI 2 tool is being launched 866 * 867 * @return string A role string suitable for passing with an LTI launch 868 */ 869 function lti_get_ims_role($user, $cmid, $courseid, $islti2) { 870 $roles = array(); 871 872 if (empty($cmid)) { 873 // If no cmid is passed, check if the user is a teacher in the course 874 // This allows other modules to programmatically "fake" a launch without 875 // a real LTI instance. 876 $coursecontext = context_course::instance($courseid); 877 878 if (has_capability('moodle/course:manageactivities', $coursecontext)) { 879 array_push($roles, 'Instructor'); 880 } else { 881 array_push($roles, 'Learner'); 882 } 883 } else { 884 $context = context_module::instance($cmid); 885 886 if (has_capability('mod/lti:manage', $context)) { 887 array_push($roles, 'Instructor'); 888 } else { 889 array_push($roles, 'Learner'); 890 } 891 } 892 893 if (is_siteadmin($user)) { 894 if (!$islti2) { 895 array_push($roles, 'urn:lti:sysrole:ims/lis/Administrator', 'urn:lti:instrole:ims/lis/Administrator'); 896 } else { 897 array_push($roles, 'http://purl.imsglobal.org/vocab/lis/v2/person#Administrator'); 898 } 899 } 900 901 return join(',', $roles); 902 } 903 904 /** 905 * Returns configuration details for the tool 906 * 907 * @param int $typeid Basic LTI tool typeid 908 * 909 * @return array Tool Configuration 910 */ 911 function lti_get_type_config($typeid) { 912 global $DB; 913 914 $query = "SELECT name, value 915 FROM {lti_types_config} 916 WHERE typeid = :typeid1 917 UNION ALL 918 SELECT 'toolurl' AS name, " . $DB->sql_compare_text('baseurl', 1333) . " AS value 919 FROM {lti_types} 920 WHERE id = :typeid2"; 921 922 $typeconfig = array(); 923 $configs = $DB->get_records_sql($query, array('typeid1' => $typeid, 'typeid2' => $typeid)); 924 925 if (!empty($configs)) { 926 foreach ($configs as $config) { 927 $typeconfig[$config->name] = $config->value; 928 } 929 } 930 931 return $typeconfig; 932 } 933 934 function lti_get_tools_by_url($url, $state, $courseid = null) { 935 $domain = lti_get_domain_from_url($url); 936 937 return lti_get_tools_by_domain($domain, $state, $courseid); 938 } 939 940 function lti_get_tools_by_domain($domain, $state = null, $courseid = null) { 941 global $DB, $SITE; 942 943 $filters = array('tooldomain' => $domain); 944 945 $statefilter = ''; 946 $coursefilter = ''; 947 948 if ($state) { 949 $statefilter = 'AND state = :state'; 950 } 951 952 if ($courseid && $courseid != $SITE->id) { 953 $coursefilter = 'OR course = :courseid'; 954 } 955 956 $query = "SELECT * 957 FROM {lti_types} 958 WHERE tooldomain = :tooldomain 959 AND (course = :siteid $coursefilter) 960 $statefilter"; 961 962 return $DB->get_records_sql($query, array( 963 'courseid' => $courseid, 964 'siteid' => $SITE->id, 965 'tooldomain' => $domain, 966 'state' => $state 967 )); 968 } 969 970 /** 971 * Returns all basicLTI tools configured by the administrator 972 * 973 */ 974 function lti_filter_get_types($course) { 975 global $DB; 976 977 if (!empty($course)) { 978 $where = "WHERE t.course = :course"; 979 $params = array('course' => $course); 980 } else { 981 $where = ''; 982 $params = array(); 983 } 984 $query = "SELECT t.id, t.name, t.baseurl, t.state, t.toolproxyid, t.timecreated, tp.name tpname 985 FROM {lti_types} t LEFT OUTER JOIN {lti_tool_proxies} tp ON t.toolproxyid = tp.id 986 {$where}"; 987 return $DB->get_records_sql($query, $params); 988 } 989 990 /** 991 * Given an array of tools, filter them based on their state 992 * 993 * @param array $tools An array of lti_types records 994 * @param int $state One of the LTI_TOOL_STATE_* constants 995 * @return array 996 */ 997 function lti_filter_tool_types(array $tools, $state) { 998 $return = array(); 999 foreach ($tools as $key => $tool) { 1000 if ($tool->state == $state) { 1001 $return[$key] = $tool; 1002 } 1003 } 1004 return $return; 1005 } 1006 1007 function lti_get_types_for_add_instance() { 1008 global $DB, $SITE, $COURSE; 1009 1010 $query = "SELECT * 1011 FROM {lti_types} 1012 WHERE coursevisible = 1 1013 AND (course = :siteid OR course = :courseid) 1014 AND state = :active"; 1015 1016 $admintypes = $DB->get_records_sql($query, 1017 array('siteid' => $SITE->id, 'courseid' => $COURSE->id, 'active' => LTI_TOOL_STATE_CONFIGURED)); 1018 1019 $types = array(); 1020 $types[0] = (object)array('name' => get_string('automatic', 'lti'), 'course' => 0, 'toolproxyid' => null); 1021 1022 foreach ($admintypes as $type) { 1023 $types[$type->id] = $type; 1024 } 1025 1026 return $types; 1027 } 1028 1029 function lti_get_domain_from_url($url) { 1030 $matches = array(); 1031 1032 if (preg_match(LTI_URL_DOMAIN_REGEX, $url, $matches)) { 1033 return $matches[1]; 1034 } 1035 } 1036 1037 function lti_get_tool_by_url_match($url, $courseid = null, $state = LTI_TOOL_STATE_CONFIGURED) { 1038 $possibletools = lti_get_tools_by_url($url, $state, $courseid); 1039 1040 return lti_get_best_tool_by_url($url, $possibletools, $courseid); 1041 } 1042 1043 function lti_get_url_thumbprint($url) { 1044 // Parse URL requires a schema otherwise everything goes into 'path'. Fixed 5.4.7 or later. 1045 if (preg_match('/https?:\/\//', $url) !== 1) { 1046 $url = 'http://'.$url; 1047 } 1048 $urlparts = parse_url(strtolower($url)); 1049 if (!isset($urlparts['path'])) { 1050 $urlparts['path'] = ''; 1051 } 1052 1053 if (!isset($urlparts['host'])) { 1054 $urlparts['host'] = ''; 1055 } 1056 1057 if (substr($urlparts['host'], 0, 4) === 'www.') { 1058 $urlparts['host'] = substr($urlparts['host'], 4); 1059 } 1060 1061 return $urllower = $urlparts['host'] . '/' . $urlparts['path']; 1062 } 1063 1064 function lti_get_best_tool_by_url($url, $tools, $courseid = null) { 1065 if (count($tools) === 0) { 1066 return null; 1067 } 1068 1069 $urllower = lti_get_url_thumbprint($url); 1070 1071 foreach ($tools as $tool) { 1072 $tool->_matchscore = 0; 1073 1074 $toolbaseurllower = lti_get_url_thumbprint($tool->baseurl); 1075 1076 if ($urllower === $toolbaseurllower) { 1077 // 100 points for exact thumbprint match. 1078 $tool->_matchscore += 100; 1079 } else if (substr($urllower, 0, strlen($toolbaseurllower)) === $toolbaseurllower) { 1080 // 50 points if tool thumbprint starts with the base URL thumbprint. 1081 $tool->_matchscore += 50; 1082 } 1083 1084 // Prefer course tools over site tools. 1085 if (!empty($courseid)) { 1086 // Minus 10 points for not matching the course id (global tools). 1087 if ($tool->course != $courseid) { 1088 $tool->_matchscore -= 10; 1089 } 1090 } 1091 } 1092 1093 $bestmatch = array_reduce($tools, function($value, $tool) { 1094 if ($tool->_matchscore > $value->_matchscore) { 1095 return $tool; 1096 } else { 1097 return $value; 1098 } 1099 1100 }, (object)array('_matchscore' => -1)); 1101 1102 // None of the tools are suitable for this URL. 1103 if ($bestmatch->_matchscore <= 0) { 1104 return null; 1105 } 1106 1107 return $bestmatch; 1108 } 1109 1110 function lti_get_shared_secrets_by_key($key) { 1111 global $DB; 1112 1113 // Look up the shared secret for the specified key in both the types_config table (for configured tools) 1114 // And in the lti resource table for ad-hoc tools. 1115 $query = "SELECT t2.value 1116 FROM {lti_types_config} t1 1117 JOIN {lti_types_config} t2 ON t1.typeid = t2.typeid 1118 JOIN {lti_types} type ON t2.typeid = type.id 1119 WHERE t1.name = 'resourcekey' 1120 AND t1.value = :key1 1121 AND t2.name = 'password' 1122 AND type.state = :configured1 1123 UNION 1124 SELECT tp.secret AS value 1125 FROM {lti_tool_proxies} tp 1126 JOIN {lti_types} t ON tp.id = t.toolproxyid 1127 WHERE tp.guid = :key2 1128 AND t.state = :configured2 1129 UNION 1130 SELECT password AS value 1131 FROM {lti} 1132 WHERE resourcekey = :key3"; 1133 1134 $sharedsecrets = $DB->get_records_sql($query, array('configured1' => LTI_TOOL_STATE_CONFIGURED, 1135 'configured2' => LTI_TOOL_STATE_CONFIGURED, 'key1' => $key, 'key2' => $key, 'key3' => $key)); 1136 1137 $values = array_map(function($item) { 1138 return $item->value; 1139 }, $sharedsecrets); 1140 1141 // There should really only be one shared secret per key. But, we can't prevent 1142 // more than one getting entered. For instance, if the same key is used for two tool providers. 1143 return $values; 1144 } 1145 1146 /** 1147 * Delete a Basic LTI configuration 1148 * 1149 * @param int $id Configuration id 1150 */ 1151 function lti_delete_type($id) { 1152 global $DB; 1153 1154 // We should probably just copy the launch URL to the tool instances in this case... using a single query. 1155 /* 1156 $instances = $DB->get_records('lti', array('typeid' => $id)); 1157 foreach ($instances as $instance) { 1158 $instance->typeid = 0; 1159 $DB->update_record('lti', $instance); 1160 }*/ 1161 1162 $DB->delete_records('lti_types', array('id' => $id)); 1163 $DB->delete_records('lti_types_config', array('typeid' => $id)); 1164 } 1165 1166 function lti_set_state_for_type($id, $state) { 1167 global $DB; 1168 1169 $DB->update_record('lti_types', array('id' => $id, 'state' => $state)); 1170 } 1171 1172 /** 1173 * Transforms a basic LTI object to an array 1174 * 1175 * @param object $ltiobject Basic LTI object 1176 * 1177 * @return array Basic LTI configuration details 1178 */ 1179 function lti_get_config($ltiobject) { 1180 $typeconfig = array(); 1181 $typeconfig = (array)$ltiobject; 1182 $additionalconfig = lti_get_type_config($ltiobject->typeid); 1183 $typeconfig = array_merge($typeconfig, $additionalconfig); 1184 return $typeconfig; 1185 } 1186 1187 /** 1188 * 1189 * Generates some of the tool configuration based on the instance details 1190 * 1191 * @param int $id 1192 * 1193 * @return Instance configuration 1194 * 1195 */ 1196 function lti_get_type_config_from_instance($id) { 1197 global $DB; 1198 1199 $instance = $DB->get_record('lti', array('id' => $id)); 1200 $config = lti_get_config($instance); 1201 1202 $type = new \stdClass(); 1203 $type->lti_fix = $id; 1204 if (isset($config['toolurl'])) { 1205 $type->lti_toolurl = $config['toolurl']; 1206 } 1207 if (isset($config['instructorchoicesendname'])) { 1208 $type->lti_sendname = $config['instructorchoicesendname']; 1209 } 1210 if (isset($config['instructorchoicesendemailaddr'])) { 1211 $type->lti_sendemailaddr = $config['instructorchoicesendemailaddr']; 1212 } 1213 if (isset($config['instructorchoiceacceptgrades'])) { 1214 $type->lti_acceptgrades = $config['instructorchoiceacceptgrades']; 1215 } 1216 if (isset($config['instructorchoiceallowroster'])) { 1217 $type->lti_allowroster = $config['instructorchoiceallowroster']; 1218 } 1219 1220 if (isset($config['instructorcustomparameters'])) { 1221 $type->lti_allowsetting = $config['instructorcustomparameters']; 1222 } 1223 return $type; 1224 } 1225 1226 /** 1227 * Generates some of the tool configuration based on the admin configuration details 1228 * 1229 * @param int $id 1230 * 1231 * @return Configuration details 1232 */ 1233 function lti_get_type_type_config($id) { 1234 global $DB; 1235 1236 $basicltitype = $DB->get_record('lti_types', array('id' => $id)); 1237 $config = lti_get_type_config($id); 1238 1239 $type = new \stdClass(); 1240 1241 $type->lti_typename = $basicltitype->name; 1242 1243 $type->typeid = $basicltitype->id; 1244 1245 $type->toolproxyid = $basicltitype->toolproxyid; 1246 1247 $type->lti_toolurl = $basicltitype->baseurl; 1248 1249 $type->lti_parameters = $basicltitype->parameter; 1250 1251 if (isset($config['resourcekey'])) { 1252 $type->lti_resourcekey = $config['resourcekey']; 1253 } 1254 if (isset($config['password'])) { 1255 $type->lti_password = $config['password']; 1256 } 1257 1258 if (isset($config['sendname'])) { 1259 $type->lti_sendname = $config['sendname']; 1260 } 1261 if (isset($config['instructorchoicesendname'])) { 1262 $type->lti_instructorchoicesendname = $config['instructorchoicesendname']; 1263 } 1264 if (isset($config['sendemailaddr'])) { 1265 $type->lti_sendemailaddr = $config['sendemailaddr']; 1266 } 1267 if (isset($config['instructorchoicesendemailaddr'])) { 1268 $type->lti_instructorchoicesendemailaddr = $config['instructorchoicesendemailaddr']; 1269 } 1270 if (isset($config['acceptgrades'])) { 1271 $type->lti_acceptgrades = $config['acceptgrades']; 1272 } 1273 if (isset($config['instructorchoiceacceptgrades'])) { 1274 $type->lti_instructorchoiceacceptgrades = $config['instructorchoiceacceptgrades']; 1275 } 1276 if (isset($config['allowroster'])) { 1277 $type->lti_allowroster = $config['allowroster']; 1278 } 1279 if (isset($config['instructorchoiceallowroster'])) { 1280 $type->lti_instructorchoiceallowroster = $config['instructorchoiceallowroster']; 1281 } 1282 1283 if (isset($config['customparameters'])) { 1284 $type->lti_customparameters = $config['customparameters']; 1285 } 1286 1287 if (isset($config['forcessl'])) { 1288 $type->lti_forcessl = $config['forcessl']; 1289 } 1290 1291 if (isset($config['organizationid'])) { 1292 $type->lti_organizationid = $config['organizationid']; 1293 } 1294 if (isset($config['organizationurl'])) { 1295 $type->lti_organizationurl = $config['organizationurl']; 1296 } 1297 if (isset($config['organizationdescr'])) { 1298 $type->lti_organizationdescr = $config['organizationdescr']; 1299 } 1300 if (isset($config['launchcontainer'])) { 1301 $type->lti_launchcontainer = $config['launchcontainer']; 1302 } 1303 1304 if (isset($config['coursevisible'])) { 1305 $type->lti_coursevisible = $config['coursevisible']; 1306 } 1307 1308 if (isset($config['debuglaunch'])) { 1309 $type->lti_debuglaunch = $config['debuglaunch']; 1310 } 1311 1312 if (isset($config['module_class_type'])) { 1313 $type->lti_module_class_type = $config['module_class_type']; 1314 } 1315 1316 return $type; 1317 } 1318 1319 function lti_prepare_type_for_save($type, $config) { 1320 if (isset($config->lti_toolurl)) { 1321 $type->baseurl = $config->lti_toolurl; 1322 $type->tooldomain = lti_get_domain_from_url($config->lti_toolurl); 1323 } 1324 if (isset($config->lti_typename)) { 1325 $type->name = $config->lti_typename; 1326 } 1327 $type->coursevisible = !empty($config->lti_coursevisible) ? $config->lti_coursevisible : 0; 1328 $config->lti_coursevisible = $type->coursevisible; 1329 1330 if (isset($config->lti_forcessl)) { 1331 $type->forcessl = !empty($config->lti_forcessl) ? $config->lti_forcessl : 0; 1332 $config->lti_forcessl = $type->forcessl; 1333 } 1334 1335 $type->timemodified = time(); 1336 1337 unset ($config->lti_typename); 1338 unset ($config->lti_toolurl); 1339 } 1340 1341 function lti_update_type($type, $config) { 1342 global $DB; 1343 1344 lti_prepare_type_for_save($type, $config); 1345 1346 if ($DB->update_record('lti_types', $type)) { 1347 foreach ($config as $key => $value) { 1348 if (substr($key, 0, 4) == 'lti_' && !is_null($value)) { 1349 $record = new \StdClass(); 1350 $record->typeid = $type->id; 1351 $record->name = substr($key, 4); 1352 $record->value = $value; 1353 lti_update_config($record); 1354 } 1355 } 1356 } 1357 } 1358 1359 function lti_add_type($type, $config) { 1360 global $USER, $SITE, $DB; 1361 1362 lti_prepare_type_for_save($type, $config); 1363 1364 if (!isset($type->state)) { 1365 $type->state = LTI_TOOL_STATE_PENDING; 1366 } 1367 1368 if (!isset($type->timecreated)) { 1369 $type->timecreated = time(); 1370 } 1371 1372 if (!isset($type->createdby)) { 1373 $type->createdby = $USER->id; 1374 } 1375 1376 if (!isset($type->course)) { 1377 $type->course = $SITE->id; 1378 } 1379 1380 // Create a salt value to be used for signing passed data to extension services 1381 // The outcome service uses the service salt on the instance. This can be used 1382 // for communication with services not related to a specific LTI instance. 1383 $config->lti_servicesalt = uniqid('', true); 1384 1385 $id = $DB->insert_record('lti_types', $type); 1386 1387 if ($id) { 1388 foreach ($config as $key => $value) { 1389 if (substr($key, 0, 4) == 'lti_' && !is_null($value)) { 1390 $record = new \StdClass(); 1391 $record->typeid = $id; 1392 $record->name = substr($key, 4); 1393 $record->value = $value; 1394 1395 lti_add_config($record); 1396 } 1397 } 1398 } 1399 1400 return $id; 1401 } 1402 1403 /** 1404 * Given an array of tool proxies, filter them based on their state 1405 * 1406 * @param array $toolproxies An array of lti_tool_proxies records 1407 * @param int $state One of the LTI_TOOL_PROXY_STATE_* constants 1408 * 1409 * @return array 1410 */ 1411 function lti_filter_tool_proxy_types(array $toolproxies, $state) { 1412 $return = array(); 1413 foreach ($toolproxies as $key => $toolproxy) { 1414 if ($toolproxy->state == $state) { 1415 $return[$key] = $toolproxy; 1416 } 1417 } 1418 return $return; 1419 } 1420 1421 /** 1422 * Get the tool proxy instance given its GUID 1423 * 1424 * @param string $toolproxyguid Tool proxy GUID value 1425 * 1426 * @return object 1427 */ 1428 function lti_get_tool_proxy_from_guid($toolproxyguid) { 1429 global $DB; 1430 1431 $toolproxy = $DB->get_record('lti_tool_proxies', array('guid' => $toolproxyguid)); 1432 1433 return $toolproxy; 1434 } 1435 1436 /** 1437 * Generates some of the tool proxy configuration based on the admin configuration details 1438 * 1439 * @param int $id 1440 * 1441 * @return Tool Proxy details 1442 */ 1443 function lti_get_tool_proxy($id) { 1444 global $DB; 1445 1446 $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $id)); 1447 return $toolproxy; 1448 } 1449 1450 /** 1451 * Generates some of the tool proxy configuration based on the admin configuration details 1452 * 1453 * @param int $id 1454 * 1455 * @return Tool Proxy details 1456 */ 1457 function lti_get_tool_proxy_config($id) { 1458 $toolproxy = lti_get_tool_proxy($id); 1459 1460 $tp = new \stdClass(); 1461 $tp->lti_registrationname = $toolproxy->name; 1462 $tp->toolproxyid = $toolproxy->id; 1463 $tp->state = $toolproxy->state; 1464 $tp->lti_registrationurl = $toolproxy->regurl; 1465 $tp->lti_capabilities = explode("\n", $toolproxy->capabilityoffered); 1466 $tp->lti_services = explode("\n", $toolproxy->serviceoffered); 1467 1468 return $tp; 1469 } 1470 1471 /** 1472 * Update the database with a tool proxy instance 1473 * 1474 * @param object $config Tool proxy definition 1475 * 1476 * @return int Record id number 1477 */ 1478 function lti_add_tool_proxy($config) { 1479 global $USER, $DB; 1480 1481 $toolproxy = new \stdClass(); 1482 if (isset($config->lti_registrationname)) { 1483 $toolproxy->name = trim($config->lti_registrationname); 1484 } 1485 if (isset($config->lti_registrationurl)) { 1486 $toolproxy->regurl = trim($config->lti_registrationurl); 1487 } 1488 if (isset($config->lti_capabilities)) { 1489 $toolproxy->capabilityoffered = implode("\n", $config->lti_capabilities); 1490 } 1491 if (isset($config->lti_services)) { 1492 $toolproxy->serviceoffered = implode("\n", $config->lti_services); 1493 } 1494 if (isset($config->toolproxyid) && !empty($config->toolproxyid)) { 1495 $toolproxy->id = $config->toolproxyid; 1496 if (!isset($toolproxy->state) || ($toolproxy->state != LTI_TOOL_PROXY_STATE_ACCEPTED)) { 1497 $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED; 1498 $toolproxy->guid = random_string(); 1499 $toolproxy->secret = random_string(); 1500 } 1501 $id = lti_update_tool_proxy($toolproxy); 1502 } else { 1503 $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED; 1504 $toolproxy->timemodified = time(); 1505 $toolproxy->timecreated = $toolproxy->timemodified; 1506 if (!isset($toolproxy->createdby)) { 1507 $toolproxy->createdby = $USER->id; 1508 } 1509 $toolproxy->guid = random_string(); 1510 $toolproxy->secret = random_string(); 1511 $id = $DB->insert_record('lti_tool_proxies', $toolproxy); 1512 } 1513 1514 return $id; 1515 } 1516 1517 /** 1518 * Updates a tool proxy in the database 1519 * 1520 * @param object $toolproxy Tool proxy 1521 * 1522 * @return int Record id number 1523 */ 1524 function lti_update_tool_proxy($toolproxy) { 1525 global $DB; 1526 1527 $toolproxy->timemodified = time(); 1528 $id = $DB->update_record('lti_tool_proxies', $toolproxy); 1529 1530 return $id; 1531 } 1532 1533 /** 1534 * Delete a Tool Proxy 1535 * 1536 * @param int $id Tool Proxy id 1537 */ 1538 function lti_delete_tool_proxy($id) { 1539 global $DB; 1540 $DB->delete_records('lti_tool_settings', array('toolproxyid' => $id)); 1541 $tools = $DB->get_records('lti_types', array('toolproxyid' => $id)); 1542 foreach ($tools as $tool) { 1543 lti_delete_type($tool->id); 1544 } 1545 $DB->delete_records('lti_tool_proxies', array('id' => $id)); 1546 } 1547 1548 /** 1549 * Add a tool configuration in the database 1550 * 1551 * @param object $config Tool configuration 1552 * 1553 * @return int Record id number 1554 */ 1555 function lti_add_config($config) { 1556 global $DB; 1557 1558 return $DB->insert_record('lti_types_config', $config); 1559 } 1560 1561 /** 1562 * Updates a tool configuration in the database 1563 * 1564 * @param object $config Tool configuration 1565 * 1566 * @return Record id number 1567 */ 1568 function lti_update_config($config) { 1569 global $DB; 1570 1571 $return = true; 1572 $old = $DB->get_record('lti_types_config', array('typeid' => $config->typeid, 'name' => $config->name)); 1573 1574 if ($old) { 1575 $config->id = $old->id; 1576 $return = $DB->update_record('lti_types_config', $config); 1577 } else { 1578 $return = $DB->insert_record('lti_types_config', $config); 1579 } 1580 return $return; 1581 } 1582 1583 /** 1584 * Gets the tool settings 1585 * 1586 * @param int $toolproxyid Id of tool proxy record 1587 * @param int $courseid Id of course (null if system settings) 1588 * @param int $instanceid Id of course module (null if system or context settings) 1589 * 1590 * @return array Array settings 1591 */ 1592 function lti_get_tool_settings($toolproxyid, $courseid = null, $instanceid = null) { 1593 global $DB; 1594 1595 $settings = array(); 1596 $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('toolproxyid' => $toolproxyid, 1597 'course' => $courseid, 'coursemoduleid' => $instanceid)); 1598 if ($settingsstr !== false) { 1599 $settings = json_decode($settingsstr, true); 1600 } 1601 return $settings; 1602 } 1603 1604 /** 1605 * Sets the tool settings ( 1606 * 1607 * @param array $settings Array of settings 1608 * @param int $toolproxyid Id of tool proxy record 1609 * @param int $courseid Id of course (null if system settings) 1610 * @param int $instanceid Id of course module (null if system or context settings) 1611 */ 1612 function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $instanceid = null) { 1613 global $DB; 1614 1615 $json = json_encode($settings); 1616 $record = $DB->get_record('lti_tool_settings', array('toolproxyid' => $toolproxyid, 1617 'course' => $courseid, 'coursemoduleid' => $instanceid)); 1618 if ($record !== false) { 1619 $DB->update_record('lti_tool_settings', array('id' => $record->id, 'settings' => $json, 'timemodified' => time())); 1620 } else { 1621 $record = new \stdClass(); 1622 $record->toolproxyid = $toolproxyid; 1623 $record->course = $courseid; 1624 $record->coursemoduleid = $instanceid; 1625 $record->settings = $json; 1626 $record->timecreated = time(); 1627 $record->timemodified = $record->timecreated; 1628 $DB->insert_record('lti_tool_settings', $record); 1629 } 1630 } 1631 1632 /** 1633 * Signs the petition to launch the external tool using OAuth 1634 * 1635 * @param $oldparms Parameters to be passed for signing 1636 * @param $endpoint url of the external tool 1637 * @param $method Method for sending the parameters (e.g. POST) 1638 * @param $oauth_consumoer_key Key 1639 * @param $oauth_consumoer_secret Secret 1640 */ 1641 function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret) { 1642 1643 $parms = $oldparms; 1644 1645 $testtoken = ''; 1646 1647 // TODO: Switch to core oauthlib once implemented - MDL-30149. 1648 $hmacmethod = new lti\OAuthSignatureMethod_HMAC_SHA1(); 1649 $testconsumer = new lti\OAuthConsumer($oauthconsumerkey, $oauthconsumersecret, null); 1650 $accreq = lti\OAuthRequest::from_consumer_and_token($testconsumer, $testtoken, $method, $endpoint, $parms); 1651 $accreq->sign_request($hmacmethod, $testconsumer, $testtoken); 1652 1653 $newparms = $accreq->get_parameters(); 1654 1655 return $newparms; 1656 } 1657 1658 /** 1659 * Posts the launch petition HTML 1660 * 1661 * @param $newparms Signed parameters 1662 * @param $endpoint URL of the external tool 1663 * @param $debug Debug (true/false) 1664 */ 1665 function lti_post_launch_html($newparms, $endpoint, $debug=false) { 1666 $r = "<form action=\"" . $endpoint . 1667 "\" name=\"ltiLaunchForm\" id=\"ltiLaunchForm\" method=\"post\" encType=\"application/x-www-form-urlencoded\">\n"; 1668 1669 // Contruct html for the launch parameters. 1670 foreach ($newparms as $key => $value) { 1671 $key = htmlspecialchars($key); 1672 $value = htmlspecialchars($value); 1673 if ( $key == "ext_submit" ) { 1674 $r .= "<input type=\"submit\""; 1675 } else { 1676 $r .= "<input type=\"hidden\" name=\"{$key}\""; 1677 } 1678 $r .= " value=\""; 1679 $r .= $value; 1680 $r .= "\"/>\n"; 1681 } 1682 1683 if ( $debug ) { 1684 $r .= "<script language=\"javascript\"> \n"; 1685 $r .= " //<![CDATA[ \n"; 1686 $r .= "function basicltiDebugToggle() {\n"; 1687 $r .= " var ele = document.getElementById(\"basicltiDebug\");\n"; 1688 $r .= " if (ele.style.display == \"block\") {\n"; 1689 $r .= " ele.style.display = \"none\";\n"; 1690 $r .= " }\n"; 1691 $r .= " else {\n"; 1692 $r .= " ele.style.display = \"block\";\n"; 1693 $r .= " }\n"; 1694 $r .= "} \n"; 1695 $r .= " //]]> \n"; 1696 $r .= "</script>\n"; 1697 $r .= "<a id=\"displayText\" href=\"javascript:basicltiDebugToggle();\">"; 1698 $r .= get_string("toggle_debug_data", "lti")."</a>\n"; 1699 $r .= "<div id=\"basicltiDebug\" style=\"display:none\">\n"; 1700 $r .= "<b>".get_string("basiclti_endpoint", "lti")."</b><br/>\n"; 1701 $r .= $endpoint . "<br/>\n <br/>\n"; 1702 $r .= "<b>".get_string("basiclti_parameters", "lti")."</b><br/>\n"; 1703 foreach ($newparms as $key => $value) { 1704 $key = htmlspecialchars($key); 1705 $value = htmlspecialchars($value); 1706 $r .= "$key = $value<br/>\n"; 1707 } 1708 $r .= " <br/>\n"; 1709 $r .= "</div>\n"; 1710 } 1711 $r .= "</form>\n"; 1712 1713 if ( ! $debug ) { 1714 $r .= " <script type=\"text/javascript\"> \n" . 1715 " //<![CDATA[ \n" . 1716 " document.ltiLaunchForm.submit(); \n" . 1717 " //]]> \n" . 1718 " </script> \n"; 1719 } 1720 return $r; 1721 } 1722 1723 function lti_get_type($typeid) { 1724 global $DB; 1725 1726 return $DB->get_record('lti_types', array('id' => $typeid)); 1727 } 1728 1729 function lti_get_launch_container($lti, $toolconfig) { 1730 if (empty($lti->launchcontainer)) { 1731 $lti->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT; 1732 } 1733 1734 if ($lti->launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) { 1735 if (isset($toolconfig['launchcontainer'])) { 1736 $launchcontainer = $toolconfig['launchcontainer']; 1737 } 1738 } else { 1739 $launchcontainer = $lti->launchcontainer; 1740 } 1741 1742 if (empty($launchcontainer) || $launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) { 1743 $launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS; 1744 } 1745 1746 $devicetype = core_useragent::get_device_type(); 1747 1748 // Scrolling within the object element doesn't work on iOS or Android 1749 // Opening the popup window also had some issues in testing 1750 // For mobile devices, always take up the entire screen to ensure the best experience. 1751 if ($devicetype === core_useragent::DEVICETYPE_MOBILE || $devicetype === core_useragent::DEVICETYPE_TABLET ) { 1752 $launchcontainer = LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW; 1753 } 1754 1755 return $launchcontainer; 1756 } 1757 1758 function lti_request_is_using_ssl() { 1759 global $CFG; 1760 return (stripos($CFG->httpswwwroot, 'https://') === 0); 1761 } 1762 1763 function lti_ensure_url_is_https($url) { 1764 if (!strstr($url, '://')) { 1765 $url = 'https://' . $url; 1766 } else { 1767 // If the URL starts with http, replace with https. 1768 if (stripos($url, 'http://') === 0) { 1769 $url = 'https://' . substr($url, 7); 1770 } 1771 } 1772 1773 return $url; 1774 } 1775 1776 /** 1777 * Determines if we should try to log the request 1778 * 1779 * @param string $rawbody 1780 * @return bool 1781 */ 1782 function lti_should_log_request($rawbody) { 1783 global $CFG; 1784 1785 if (empty($CFG->mod_lti_log_users)) { 1786 return false; 1787 } 1788 1789 $logusers = explode(',', $CFG->mod_lti_log_users); 1790 if (empty($logusers)) { 1791 return false; 1792 } 1793 1794 try { 1795 $xml = new \SimpleXMLElement($rawbody); 1796 $ns = $xml->getNamespaces(); 1797 $ns = array_shift($ns); 1798 $xml->registerXPathNamespace('lti', $ns); 1799 $requestuserid = ''; 1800 if ($node = $xml->xpath('//lti:userId')) { 1801 $node = $node[0]; 1802 $requestuserid = clean_param((string) $node, PARAM_INT); 1803 } else if ($node = $xml->xpath('//lti:sourcedId')) { 1804 $node = $node[0]; 1805 $resultjson = json_decode((string) $node); 1806 $requestuserid = clean_param($resultjson->data->userid, PARAM_INT); 1807 } 1808 } catch (Exception $e) { 1809 return false; 1810 } 1811 1812 if (empty($requestuserid) or !in_array($requestuserid, $logusers)) { 1813 return false; 1814 } 1815 1816 return true; 1817 } 1818 1819 /** 1820 * Logs the request to a file in temp dir 1821 * 1822 * @param string $rawbody 1823 */ 1824 function lti_log_request($rawbody) { 1825 if ($tempdir = make_temp_directory('mod_lti', false)) { 1826 if ($tempfile = tempnam($tempdir, 'mod_lti_request'.date('YmdHis'))) { 1827 file_put_contents($tempfile, $rawbody); 1828 chmod($tempfile, 0644); 1829 } 1830 } 1831 } 1832 1833 /** 1834 * Fetches LTI type configuration for an LTI instance 1835 * 1836 * @param stdClass $instance 1837 * @return array Can be empty if no type is found 1838 */ 1839 function lti_get_type_config_by_instance($instance) { 1840 $typeid = null; 1841 if (empty($instance->typeid)) { 1842 $tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course); 1843 if ($tool) { 1844 $typeid = $tool->id; 1845 } 1846 } else { 1847 $typeid = $instance->typeid; 1848 } 1849 if (!empty($typeid)) { 1850 return lti_get_type_config($typeid); 1851 } 1852 return array(); 1853 } 1854 1855 /** 1856 * Enforce type config settings onto the LTI instance 1857 * 1858 * @param stdClass $instance 1859 * @param array $typeconfig 1860 */ 1861 function lti_force_type_config_settings($instance, array $typeconfig) { 1862 $forced = array( 1863 'instructorchoicesendname' => 'sendname', 1864 'instructorchoicesendemailaddr' => 'sendemailaddr', 1865 'instructorchoiceacceptgrades' => 'acceptgrades', 1866 ); 1867 1868 foreach ($forced as $instanceparam => $typeconfigparam) { 1869 if (array_key_exists($typeconfigparam, $typeconfig) && $typeconfig[$typeconfigparam] != LTI_SETTING_DELEGATE) { 1870 $instance->$instanceparam = $typeconfig[$typeconfigparam]; 1871 } 1872 } 1873 } 1874 1875 /** 1876 * Initializes an array with the capabilities supported by the LTI module 1877 * 1878 * @return array List of capability names (without a dollar sign prefix) 1879 */ 1880 function lti_get_capabilities() { 1881 1882 $capabilities = array( 1883 'basic-lti-launch-request' => '', 1884 'Context.id' => 'context_id', 1885 'CourseSection.title' => 'context_title', 1886 'CourseSection.label' => 'context_label', 1887 'CourseSection.sourcedId' => 'lis_course_section_sourcedid', 1888 'CourseSection.longDescription' => '$COURSE->summary', 1889 'CourseSection.timeFrame.begin' => '$COURSE->startdate', 1890 'ResourceLink.id' => 'resource_link_id', 1891 'ResourceLink.title' => 'resource_link_title', 1892 'ResourceLink.description' => 'resource_link_description', 1893 'User.id' => 'user_id', 1894 'User.username' => '$USER->username', 1895 'Person.name.full' => 'lis_person_name_full', 1896 'Person.name.given' => 'lis_person_name_given', 1897 'Person.name.family' => 'lis_person_name_family', 1898 'Person.email.primary' => 'lis_person_contact_email_primary', 1899 'Person.sourcedId' => 'lis_person_sourcedid', 1900 'Person.name.middle' => '$USER->middlename', 1901 'Person.address.street1' => '$USER->address', 1902 'Person.address.locality' => '$USER->city', 1903 'Person.address.country' => '$USER->country', 1904 'Person.address.timezone' => '$USER->timezone', 1905 'Person.phone.primary' => '$USER->phone1', 1906 'Person.phone.mobile' => '$USER->phone2', 1907 'Person.webaddress' => '$USER->url', 1908 'Membership.role' => 'roles', 1909 'Result.sourcedId' => 'lis_result_sourcedid', 1910 'Result.autocreate' => 'lis_outcome_service_url'); 1911 1912 return $capabilities; 1913 1914 } 1915 1916 /** 1917 * Initializes an array with the services supported by the LTI module 1918 * 1919 * @return array List of services 1920 */ 1921 function lti_get_services() { 1922 1923 $services = array(); 1924 $definedservices = core_component::get_plugin_list('ltiservice'); 1925 foreach ($definedservices as $name => $location) { 1926 $classname = "\\ltiservice_{$name}\\local\\service\\{$name}"; 1927 $services[] = new $classname(); 1928 } 1929 1930 return $services; 1931 1932 } 1933 1934 /** 1935 * Initializes an instance of the named service 1936 * 1937 * @param string $servicename Name of service 1938 * 1939 * @return mod_lti\local\ltiservice\service_base Service 1940 */ 1941 function lti_get_service_by_name($servicename) { 1942 1943 $service = false; 1944 $classname = "\\ltiservice_{$servicename}\\local\\service\\{$servicename}"; 1945 if (class_exists($classname)) { 1946 $service = new $classname(); 1947 } 1948 1949 return $service; 1950 1951 } 1952 1953 /** 1954 * Finds a service by id 1955 * 1956 * @param array $services Array of services 1957 * @param string $resourceid ID of resource 1958 * 1959 * @return mod_lti\local\ltiservice\service_base Service 1960 */ 1961 function lti_get_service_by_resource_id($services, $resourceid) { 1962 1963 $service = false; 1964 foreach ($services as $aservice) { 1965 foreach ($aservice->get_resources() as $resource) { 1966 if ($resource->get_id() === $resourceid) { 1967 $service = $aservice; 1968 break 2; 1969 } 1970 } 1971 } 1972 1973 return $service; 1974 1975 } 1976 1977 /** 1978 * Extracts the named contexts from a tool proxy 1979 * 1980 * @param object $json 1981 * 1982 * @return array Contexts 1983 */ 1984 function lti_get_contexts($json) { 1985 1986 $contexts = array(); 1987 if (isset($json->{'@context'})) { 1988 foreach ($json->{'@context'} as $context) { 1989 if (is_object($context)) { 1990 $contexts = array_merge(get_object_vars($context), $contexts); 1991 } 1992 } 1993 } 1994 1995 return $contexts; 1996 1997 } 1998 1999 /** 2000 * Converts an ID to a fully-qualified ID 2001 * 2002 * @param array $contexts 2003 * @param string $id 2004 * 2005 * @return string Fully-qualified ID 2006 */ 2007 function lti_get_fqid($contexts, $id) { 2008 2009 $parts = explode(':', $id, 2); 2010 if (count($parts) > 1) { 2011 if (array_key_exists($parts[0], $contexts)) { 2012 $id = $contexts[$parts[0]] . $parts[1]; 2013 } 2014 } 2015 2016 return $id; 2017 2018 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 20:29:05 2014 | Cross-referenced by PHPXref 0.7.1 |