diff --git a/enrol/lti/classes/helper.php b/enrol/lti/classes/helper.php new file mode 100644 index 00000000000..ba554d60f65 --- /dev/null +++ b/enrol/lti/classes/helper.php @@ -0,0 +1,372 @@ +. + +/** + * LTI enrolment plugin helper. + * + * @package enrol_lti + * @copyright 2016 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace enrol_lti; + +defined('MOODLE_INTERNAL') || die(); + +/** + * LTI enrolment plugin helper class. + * + * @package enrol_lti + * @copyright 2016 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class helper { + /* + * The value used when we want to enrol new members and unenrol old ones. + */ + const MEMBER_SYNC_ENROL_AND_UNENROL = 1; + + /* + * The value used when we want to enrol new members only. + */ + const MEMBER_SYNC_ENROL_NEW = 2; + + /* + * The value used when we want to unenrol missing users. + */ + const MEMBER_SYNC_UNENROL_MISSING = 3; + + /** + * Code for when an enrolment was successful. + */ + const ENROLMENT_SUCCESSFUL = true; + + /** + * Error code for enrolment when max enrolled reached. + */ + const ENROLMENT_MAX_ENROLLED = 'maxenrolledreached'; + + /** + * Error code for enrolment has not started. + */ + const ENROLMENT_NOT_STARTED = 'enrolmentnotstarted'; + + /** + * Error code for enrolment when enrolment has finished. + */ + const ENROLMENT_FINISHED = 'enrolmentfinished'; + + /** + * Error code for when an image file fails to upload. + */ + const PROFILE_IMAGE_UPDATE_SUCCESSFUL = true; + + /** + * Error code for when an image file fails to upload. + */ + const PROFILE_IMAGE_UPDATE_FAILED = 'profileimagefailed'; + + /** + * Creates a unique username. + * + * @param string $consumerkey Consumer key + * @param string $ltiuserid External tool user id + * @return string The new username + */ + public static function create_username($consumerkey, $ltiuserid) { + if (!empty($ltiuserid) && !empty($consumerkey)) { + $userkey = $consumerkey . ':' . $ltiuserid; + } else { + $userkey = false; + } + + return 'enrol_lti' . sha1($consumerkey . '::' . $userkey); + } + + /** + * Adds default values for the user object based on the tool provided. + * + * @param \stdClass $tool + * @param \stdClass $user + * @return \stdClass The $user class with added default values + */ + public static function assign_user_tool_data($tool, $user) { + global $CFG; + + $user->city = (!empty($tool->city)) ? $tool->city : ""; + $user->country = (!empty($tool->country)) ? $tool->country : ""; + $user->institution = (!empty($tool->institution)) ? $tool->institution : ""; + $user->timezone = (!empty($tool->timezone)) ? $tool->timezone : ""; + $user->maildisplay = (!empty($tool->maildisplay)) ? $tool->maildisplay : $CFG->defaultpreference_maildisplay; + $user->mnethostid = $CFG->mnet_localhost_id; + $user->confirmed = 1; + $user->lang = $tool->lang; + + return $user; + } + + /** + * Compares two users. + * + * @param \stdClass $newuser The new user + * @param \stdClass $olduser The old user + * @return bool True if both users are the same + */ + public static function user_match($newuser, $olduser) { + if ($newuser->firstname != $olduser->firstname) { + return false; + } + if ($newuser->lastname != $olduser->lastname) { + return false; + } + if ($newuser->email != $olduser->email) { + return false; + } + if ($newuser->city != $olduser->city) { + return false; + } + if ($newuser->country != $olduser->country) { + return false; + } + if ($newuser->institution != $olduser->institution) { + return false; + } + if ($newuser->timezone != $olduser->timezone) { + return false; + } + if ($newuser->maildisplay != $olduser->maildisplay) { + return false; + } + if ($newuser->mnethostid != $olduser->mnethostid) { + return false; + } + if ($newuser->confirmed != $olduser->confirmed) { + return false; + } + if ($newuser->lang != $olduser->lang) { + return false; + } + + return true; + } + + /** + * Updates the users profile image. + * + * @param int $userid the id of the user + * @param string $url the url of the image + * @return bool|string true if successful, else a string explaining why it failed + */ + public static function update_user_profile_image($userid, $url) { + global $CFG, $DB; + + require_once($CFG->libdir . '/filelib.php'); + require_once($CFG->libdir . '/gdlib.php'); + + $fs = get_file_storage(); + + $context = \context_user::instance($userid, MUST_EXIST); + $fs->delete_area_files($context->id, 'user', 'newicon'); + + $filerecord = array( + 'contextid' => $context->id, + 'component' => 'user', + 'filearea' => 'newicon', + 'itemid' => 0, + 'filepath' => '/' + ); + + $urlparams = array( + 'calctimeout' => false, + 'timeout' => 5, + 'skipcertverify' => true, + 'connecttimeout' => 5 + ); + + if (!$iconfiles = $fs->create_file_from_url($filerecord, $url, $urlparams)) { + return self::PROFILE_IMAGE_UPDATE_FAILED; + } + + $iconfile = $fs->get_area_files($context->id, 'user', 'newicon', false, 'itemid', false); + + // There should only be one. + $iconfile = reset($iconfile); + + // Something went wrong while creating temp file - remove the uploaded file. + if (!$iconfile = $iconfile->copy_content_to_temp()) { + $fs->delete_area_files($context->id, 'user', 'newicon'); + return self::PROFILE_IMAGE_UPDATE_FAILED; + } + + // Copy file to temporary location and the send it for processing icon. + $newpicture = (int) process_new_icon($context, 'user', 'icon', 0, $iconfile); + // Delete temporary file. + @unlink($iconfile); + // Remove uploaded file. + $fs->delete_area_files($context->id, 'user', 'newicon'); + // Set the user's picture. + $DB->set_field('user', 'picture', $newpicture, array('id' => $userid)); + return self::PROFILE_IMAGE_UPDATE_SUCCESSFUL; + } + + /** + * Enrol a user in a course. + * + * @param \stdclass $tool The tool object (retrieved using self::get_lti_tool() or self::get_lti_tools()) + * @param int $userid The user id + * @return bool|string returns true if successful, else an error code + */ + public static function enrol_user($tool, $userid) { + global $DB; + + // Check if the user enrolment exists. + if (!$DB->record_exists('user_enrolments', array('enrolid' => $tool->enrolid, 'userid' => $userid))) { + // Check if the maximum enrolled limit has been met. + if ($tool->maxenrolled) { + if ($DB->count_records('user_enrolments', array('enrolid' => $tool->enrolid)) >= $tool->maxenrolled) { + return self::ENROLMENT_MAX_ENROLLED; + } + } + // Check if the enrolment has not started. + if ($tool->enrolstartdate && time() < $tool->enrolstartdate) { + return self::ENROLMENT_NOT_STARTED; + } + // Check if the enrolment has finished. + if ($tool->enrolenddate && time() > $tool->enrolenddate) { + return self::ENROLMENT_FINISHED; + } + + $timeend = 0; + if ($tool->enrolperiod) { + $timeend = time() + $tool->enrolperiod; + } + + // Finally, enrol the user. + $instance = new \stdClass(); + $instance->id = $tool->enrolid; + $instance->courseid = $tool->courseid; + $instance->enrol = 'lti'; + $instance->status = $tool->status; + $ltienrol = enrol_get_plugin('lti'); + $ltienrol->enrol_user($instance, $userid, null, time(), $timeend); + } + + return self::ENROLMENT_SUCCESSFUL; + } + + /** + * Returns the LTI tool. + * + * @param int $toolid + * @return \stdClass the tool + */ + public static function get_lti_tool($toolid) { + global $DB; + + $sql = "SELECT elt.*, e.name, e.courseid, e.status, e.enrolstartdate, e.enrolenddate, e.enrolperiod + FROM {enrol_lti_tools} elt + JOIN {enrol} e + ON elt.enrolid = e.id + WHERE elt.id = :tid"; + + return $DB->get_record_sql($sql, array('tid' => $toolid), MUST_EXIST); + } + + /** + * Returns the LTI tools requested. + * + * @param array $params The list of SQL params (eg. array('columnname' => value, 'columnname2' => value)). + * @param int $limitfrom return a subset of records, starting at this point (optional). + * @param int $limitnum return a subset comprising this many records in total + * @return array of tools + */ + public static function get_lti_tools($params = array(), $limitfrom = 0, $limitnum = 0) { + global $DB; + + $sql = "SELECT elt.*, e.name, e.courseid, e.status, e.enrolstartdate, e.enrolenddate, e.enrolperiod + FROM {enrol_lti_tools} elt + JOIN {enrol} e + ON elt.enrolid = e.id"; + if ($params) { + $where = "WHERE"; + foreach ($params as $colname => $value) { + $sql .= " $where $colname = :$colname"; + $where = "AND"; + } + } + $sql .= " ORDER BY timecreated"; + + return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum); + } + + /** + * Returns the number of LTI tools. + * + * @param array $params The list of SQL params (eg. array('columnname' => value, 'columnname2' => value)). + * @return int The number of tools + */ + public static function count_lti_tools($params = array()) { + global $DB; + + $sql = "SELECT COUNT(*) + FROM {enrol_lti_tools} elt + JOIN {enrol} e + ON elt.enrolid = e.id"; + if ($params) { + $where = "WHERE"; + foreach ($params as $colname => $value) { + $sql .= " $where $colname = :$colname"; + $where = "AND"; + } + } + + return $DB->count_records_sql($sql, $params); + } + + /** + * Create a IMS POX body request for sync grades. + * + * @param string $source Sourceid required for the request + * @param float $grade User final grade + * @return string + */ + public static function create_service_body($source, $grade) { + return ' + + + + V1.0 + ' . (time()) . ' + + + + + + + ' . $source . ' + + + + en-us + ' . $grade . ' + + + + + + '; + } +} diff --git a/enrol/lti/classes/manage_table.php b/enrol/lti/classes/manage_table.php new file mode 100644 index 00000000000..d3344839226 --- /dev/null +++ b/enrol/lti/classes/manage_table.php @@ -0,0 +1,218 @@ +. + +/** + * Displays enrolment LTI instances. + * + * @package enrol_lti + * @copyright 2016 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace enrol_lti; + +defined('MOODLE_INTERNAL') || die; + +global $CFG; + +require_once($CFG->libdir . '/tablelib.php'); + +/** + * Handles displaying enrolment LTI instances. + * + * @package enrol_lti + * @copyright 2016 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class manage_table extends \table_sql { + + /** + * @var \enrol_plugin $ltiplugin + */ + protected $ltiplugin; + + /** + * @var bool $ltienabled + */ + protected $ltienabled; + + /** + * @var bool $canconfig + */ + protected $canconfig; + + /** + * @var int $courseid The course id. + */ + protected $courseid; + + /** + * Sets up the table. + * + * @param string $courseid The id of the course. + */ + public function __construct($courseid) { + parent::__construct('enrol_lti_manage_table'); + + $this->define_columns(array( + 'name', + 'url', + 'secret', + 'edit' + )); + $this->define_headers(array( + get_string('name'), + get_string('url'), + get_string('secret', 'enrol_lti'), + get_string('edit') + )); + $this->collapsible(false); + $this->sortable(false); + + // Set the variables we need access to. + $this->ltiplugin = enrol_get_plugin('lti'); + $this->ltienabled = enrol_is_enabled('lti'); + $this->canconfig = has_capability('moodle/course:enrolconfig', \context_course::instance($courseid)); + $this->courseid = $courseid; + } + + /** + * Generate the name column. + * + * @param \stdClass $tool event data. + * @return string + */ + public function col_name($tool) { + if (empty($tool->name)) { + $toolcontext = \context::instance_by_id($tool->contextid); + $name = $toolcontext->get_context_name(); + } else { + $name = $tool->name; + }; + + return $this->get_display_text($tool, $name); + } + + /** + * Generate the URL column. + * + * @param \stdClass $tool event data. + * @return string + */ + public function col_url($tool) { + $url = new \moodle_url('/enrol/lti/tool.php', array('id' => $tool->id)); + return $this->get_display_text($tool, $url); + } + + /** + * Generate the secret column. + * + * @param \stdClass $tool event data. + * @return string + */ + public function col_secret($tool) { + return $this->get_display_text($tool, $tool->secret); + } + + + /** + * Generate the edit column. + * + * @param \stdClass $tool event data. + * @return string + */ + public function col_edit($tool) { + global $OUTPUT; + + $buttons = array(); + + $instance = new \stdClass(); + $instance->id = $tool->enrolid; + $instance->courseid = $tool->courseid; + $instance->enrol = 'lti'; + $instance->status = $tool->status; + + $strdelete = get_string('delete'); + $strenable = get_string('enable'); + $strdisable = get_string('disable'); + + $url = new \moodle_url('/enrol/lti/index.php', array('sesskey' => sesskey(), 'courseid' => $this->courseid)); + + if ($this->ltiplugin->can_delete_instance($instance)) { + $aurl = new \moodle_url($url, array('action' => 'delete', 'instanceid' => $instance->id)); + $buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/delete', $strdelete, 'core', + array('class' => 'iconsmall'))); + } + + if ($this->ltienabled && $this->ltiplugin->can_hide_show_instance($instance)) { + if ($instance->status == ENROL_INSTANCE_ENABLED) { + $aurl = new \moodle_url($url, array('action' => 'disable', 'instanceid' => $instance->id)); + $buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/hide', $strdisable, 'core', + array('class' => 'iconsmall'))); + } else if ($instance->status == ENROL_INSTANCE_DISABLED) { + $aurl = new \moodle_url($url, array('action' => 'enable', 'instanceid' => $instance->id)); + $buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/show', $strenable, 'core', + array('class' => 'iconsmall'))); + } + } + + if ($this->ltienabled && $this->canconfig) { + $linkparams = array( + 'courseid' => $instance->courseid, + 'id' => $instance->id, 'type' => $instance->enrol, + 'returnurl' => new \moodle_url('/enrol/lti/index.php', array('courseid' => $this->courseid)) + ); + $editlink = new \moodle_url("/enrol/editinstance.php", $linkparams); + $buttons[] = $OUTPUT->action_icon($editlink, new \pix_icon('t/edit', get_string('edit'), 'core', + array('class' => 'iconsmall'))); + } + + return implode(' ', $buttons); + } + + /** + * Query the reader. Store results in the object for use by build_table. + * + * @param int $pagesize size of page for paginated displayed table. + * @param bool $useinitialsbar do you want to use the initials bar. + */ + public function query_db($pagesize, $useinitialsbar = true) { + $total = \enrol_lti\helper::count_lti_tools(array('courseid' => $this->courseid)); + $this->pagesize($pagesize, $total); + $tools = \enrol_lti\helper::get_lti_tools(array('courseid' => $this->courseid), $this->get_page_start(), + $this->get_page_size()); + $this->rawdata = $tools; + // Set initial bars. + if ($useinitialsbar) { + $this->initialbars($total > $pagesize); + } + } + + /** + * Returns text to display in the columns. + * + * @param \stdClass $tool the tool + * @param string $text the text to alter + * @return string + */ + protected function get_display_text($tool, $text) { + if ($tool->status != ENROL_INSTANCE_ENABLED) { + return \html_writer::tag('span', $text, array('class' => 'dimmed_text')); + } + + return $text; + } +} diff --git a/enrol/lti/db/access.php b/enrol/lti/db/access.php new file mode 100644 index 00000000000..b1a16e96540 --- /dev/null +++ b/enrol/lti/db/access.php @@ -0,0 +1,47 @@ +. + +/** + * Capabilities for LTI enrolment plugin. + * + * @package enrol_lti + * @copyright 2016 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + /* Add, edit or remove lti enrol instance. */ + 'enrol/lti:config' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ) + ), + + 'enrol/lti:unenrol' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ) + ), +); diff --git a/enrol/lti/db/install.xml b/enrol/lti/db/install.xml new file mode 100755 index 00000000000..9a221842c03 --- /dev/null +++ b/enrol/lti/db/install.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/enrol/lti/ims-blti/LICENSE.txt b/enrol/lti/ims-blti/LICENSE.txt new file mode 100644 index 00000000000..89f0591697f --- /dev/null +++ b/enrol/lti/ims-blti/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License + +Copyright (c) 2007 Andy Smith + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/enrol/lti/ims-blti/OAuth.php b/enrol/lti/ims-blti/OAuth.php new file mode 100644 index 00000000000..3077787a417 --- /dev/null +++ b/enrol/lti/ims-blti/OAuth.php @@ -0,0 +1,808 @@ +key = $key; + $this->secret = $secret; + $this->callback_url = $callback_url; + } + + function __toString() { + return "OAuthConsumer[key=$this->key,secret=$this->secret]"; + } +} + +class OAuthToken { + // access tokens and request tokens + public $key; + public $secret; + + /** + * key = the token + * secret = the token secret + */ + function __construct($key, $secret) { + $this->key = $key; + $this->secret = $secret; + } + + /** + * generates the basic string serialization of a token that a server + * would respond to request_token and access_token calls with + */ + function to_string() { + return "oauth_token=" . + OAuthUtil::urlencode_rfc3986($this->key) . + "&oauth_token_secret=" . + OAuthUtil::urlencode_rfc3986($this->secret); + } + + function __toString() { + return $this->to_string(); + } +} + +class OAuthSignatureMethod { + public function check_signature(&$request, $consumer, $token, $signature) { + $built = $this->build_signature($request, $consumer, $token); + return $built == $signature; + } +} + +class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod { + function get_name() { + return "HMAC-SHA1"; + } + + public function build_signature($request, $consumer, $token) { + global $OAuth_last_computed_signature; + $OAuth_last_computed_signature = false; + + $base_string = $request->get_signature_base_string(); + $request->base_string = $base_string; + + $key_parts = array( + $consumer->secret, + ($token) ? $token->secret : "" + ); + + $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); + $key = implode('&', $key_parts); + + $computed_signature = base64_encode(hash_hmac('sha1', $base_string, $key, true)); + $OAuth_last_computed_signature = $computed_signature; + return $computed_signature; + } + +} + +class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod { + public function get_name() { + return "PLAINTEXT"; + } + + public function build_signature($request, $consumer, $token) { + $sig = array( + OAuthUtil::urlencode_rfc3986($consumer->secret) + ); + + if ($token) { + array_push($sig, OAuthUtil::urlencode_rfc3986($token->secret)); + } else { + array_push($sig, ''); + } + + $raw = implode("&", $sig); + // for debug purposes + $request->base_string = $raw; + + return OAuthUtil::urlencode_rfc3986($raw); + } +} + +class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod { + public function get_name() { + return "RSA-SHA1"; + } + + protected function fetch_public_cert(&$request) { + // not implemented yet, ideas are: + // (1) do a lookup in a table of trusted certs keyed off of consumer + // (2) fetch via http using a url provided by the requester + // (3) some sort of specific discovery code based on request + // + // either way should return a string representation of the certificate + throw Exception("fetch_public_cert not implemented"); + } + + protected function fetch_private_cert(&$request) { + // not implemented yet, ideas are: + // (1) do a lookup in a table of trusted certs keyed off of consumer + // + // either way should return a string representation of the certificate + throw Exception("fetch_private_cert not implemented"); + } + + public function build_signature(&$request, $consumer, $token) { + $base_string = $request->get_signature_base_string(); + $request->base_string = $base_string; + + // Fetch the private key cert based on the request + $cert = $this->fetch_private_cert($request); + + // Pull the private key ID from the certificate + $privatekeyid = openssl_get_privatekey($cert); + + // Sign using the key + $ok = openssl_sign($base_string, $signature, $privatekeyid); + + // Release the key resource + openssl_free_key($privatekeyid); + + return base64_encode($signature); + } + + public function check_signature(&$request, $consumer, $token, $signature) { + $decoded_sig = base64_decode($signature); + + $base_string = $request->get_signature_base_string(); + + // Fetch the public key cert based on the request + $cert = $this->fetch_public_cert($request); + + // Pull the public key ID from the certificate + $publickeyid = openssl_get_publickey($cert); + + // Check the computed signature against the one passed in the query + $ok = openssl_verify($base_string, $decoded_sig, $publickeyid); + + // Release the key resource + openssl_free_key($publickeyid); + + return $ok == 1; + } +} + +class OAuthRequest { + private $parameters; + private $http_method; + private $http_url; + // for debug purposes + public $base_string; + public static $version = '1.0'; + public static $POST_INPUT = 'php://input'; + + function __construct($http_method, $http_url, $parameters=NULL) { + @$parameters or $parameters = array(); + $this->parameters = $parameters; + $this->http_method = $http_method; + $this->http_url = $http_url; + } + + + /** + * attempt to build up a request from what was passed to the server + */ + public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) { + $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") + ? 'http' + : 'https'; + $port = ""; + if ( $_SERVER['SERVER_PORT'] != "80" && $_SERVER['SERVER_PORT'] != "443" && + strpos(':', $_SERVER['HTTP_HOST']) < 0 ) { + $port = ':' . $_SERVER['SERVER_PORT'] ; + } + @$http_url or $http_url = $scheme . + '://' . $_SERVER['HTTP_HOST'] . + $port . + $_SERVER['REQUEST_URI']; + @$http_method or $http_method = $_SERVER['REQUEST_METHOD']; + + // We weren't handed any parameters, so let's find the ones relevant to + // this request. + // If you run XML-RPC or similar you should use this to provide your own + // parsed parameter-list + if (!$parameters) { + // Find request headers + $request_headers = OAuthUtil::get_headers(); + + // Parse the query-string to find GET parameters + $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']); + + $ourpost = $_POST; + // Deal with magic_quotes + // http://www.php.net/manual/en/security.magicquotes.disabling.php + if ( get_magic_quotes_gpc() ) { + $outpost = array(); + foreach ($_POST as $k => $v) { + $v = stripslashes($v); + $ourpost[$k] = $v; + } + } + // Add POST Parameters if they exist + $parameters = array_merge($parameters, $ourpost); + + // We have a Authorization-header with OAuth data. Parse the header + // and add those overriding any duplicates from GET or POST + if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") { + $header_parameters = OAuthUtil::split_header( + $request_headers['Authorization'] + ); + $parameters = array_merge($parameters, $header_parameters); + } + + } + + return new OAuthRequest($http_method, $http_url, $parameters); + } + + /** + * pretty much a helper function to set up the request + */ + public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) { + @$parameters or $parameters = array(); + $defaults = array("oauth_version" => OAuthRequest::$version, + "oauth_nonce" => OAuthRequest::generate_nonce(), + "oauth_timestamp" => OAuthRequest::generate_timestamp(), + "oauth_consumer_key" => $consumer->key); + if ($token) + $defaults['oauth_token'] = $token->key; + + $parameters = array_merge($defaults, $parameters); + + // Parse the query-string to find and add GET parameters + $parts = parse_url($http_url); + if ( !empty($parts['query']) ) { + $qparms = OAuthUtil::parse_parameters($parts['query']); + $parameters = array_merge($qparms, $parameters); + } + + + return new OAuthRequest($http_method, $http_url, $parameters); + } + + public function set_parameter($name, $value, $allow_duplicates = true) { + if ($allow_duplicates && isset($this->parameters[$name])) { + // We have already added parameter(s) with this name, so add to the list + if (is_scalar($this->parameters[$name])) { + // This is the first duplicate, so transform scalar (string) + // into an array so we can add the duplicates + $this->parameters[$name] = array($this->parameters[$name]); + } + + $this->parameters[$name][] = $value; + } else { + $this->parameters[$name] = $value; + } + } + + public function get_parameter($name) { + return isset($this->parameters[$name]) ? $this->parameters[$name] : null; + } + + public function get_parameters() { + return $this->parameters; + } + + public function unset_parameter($name) { + unset($this->parameters[$name]); + } + + /** + * The request parameters, sorted and concatenated into a normalized string. + * @return string + */ + public function get_signable_parameters() { + // Grab all parameters + $params = $this->parameters; + + // Remove oauth_signature if present + // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") + if (isset($params['oauth_signature'])) { + unset($params['oauth_signature']); + } + + return OAuthUtil::build_http_query($params); + } + + /** + * Returns the base string of this request + * + * The base string defined as the method, the url + * and the parameters (normalized), each urlencoded + * and the concated with &. + */ + public function get_signature_base_string() { + $parts = array( + $this->get_normalized_http_method(), + $this->get_normalized_http_url(), + $this->get_signable_parameters() + ); + + $parts = OAuthUtil::urlencode_rfc3986($parts); + + return implode('&', $parts); + } + + /** + * just uppercases the http method + */ + public function get_normalized_http_method() { + return strtoupper($this->http_method); + } + + /** + * parses the url and rebuilds it to be + * scheme://host/path + */ + public function get_normalized_http_url() { + $parts = parse_url($this->http_url); + + $port = @$parts['port']; + $scheme = $parts['scheme']; + $host = $parts['host']; + $path = @$parts['path']; + + $port or $port = ($scheme == 'https') ? '443' : '80'; + + if (($scheme == 'https' && $port != '443') + || ($scheme == 'http' && $port != '80')) { + $host = "$host:$port"; + } + return "$scheme://$host$path"; + } + + /** + * builds a url usable for a GET request + */ + public function to_url() { + $post_data = $this->to_postdata(); + $out = $this->get_normalized_http_url(); + if ($post_data) { + $out .= '?'.$post_data; + } + return $out; + } + + /** + * builds the data one would send in a POST request + */ + public function to_postdata() { + return OAuthUtil::build_http_query($this->parameters); + } + + /** + * builds the Authorization: header + */ + public function to_header() { + $out ='Authorization: OAuth realm=""'; + $total = array(); + foreach ($this->parameters as $k => $v) { + if (substr($k, 0, 5) != "oauth") continue; + if (is_array($v)) { + throw new OAuthException('Arrays not supported in headers'); + } + $out .= ',' . + OAuthUtil::urlencode_rfc3986($k) . + '="' . + OAuthUtil::urlencode_rfc3986($v) . + '"'; + } + return $out; + } + + public function __toString() { + return $this->to_url(); + } + + + public function sign_request($signature_method, $consumer, $token) { + $this->set_parameter( + "oauth_signature_method", + $signature_method->get_name(), + false + ); + $signature = $this->build_signature($signature_method, $consumer, $token); + $this->set_parameter("oauth_signature", $signature, false); + } + + public function build_signature($signature_method, $consumer, $token) { + $signature = $signature_method->build_signature($this, $consumer, $token); + return $signature; + } + + /** + * util function: current timestamp + */ + private static function generate_timestamp() { + return time(); + } + + /** + * util function: current nonce + */ + private static function generate_nonce() { + $mt = microtime(); + $rand = mt_rand(); + + return md5($mt . $rand); // md5s look nicer than numbers + } +} + +class OAuthServer { + protected $timestamp_threshold = 300; // in seconds, five minutes + protected $version = 1.0; // hi blaine + protected $signature_methods = array(); + + protected $data_store; + + function __construct($data_store) { + $this->data_store = $data_store; + } + + public function add_signature_method($signature_method) { + $this->signature_methods[$signature_method->get_name()] = + $signature_method; + } + + // high level functions + + /** + * process a request_token request + * returns the request token on success + */ + public function fetch_request_token(&$request) { + $this->get_version($request); + + $consumer = $this->get_consumer($request); + + // no token required for the initial token request + $token = NULL; + + $this->check_signature($request, $consumer, $token); + + $new_token = $this->data_store->new_request_token($consumer); + + return $new_token; + } + + /** + * process an access_token request + * returns the access token on success + */ + public function fetch_access_token(&$request) { + $this->get_version($request); + + $consumer = $this->get_consumer($request); + + // requires authorized request token + $token = $this->get_token($request, $consumer, "request"); + + + $this->check_signature($request, $consumer, $token); + + $new_token = $this->data_store->new_access_token($token, $consumer); + + return $new_token; + } + + /** + * verify an api call, checks all the parameters + */ + public function verify_request(&$request) { + global $OAuth_last_computed_signature; + $OAuth_last_computed_signature = false; + $this->get_version($request); + $consumer = $this->get_consumer($request); + $token = $this->get_token($request, $consumer, "access"); + $this->check_signature($request, $consumer, $token); + return array($consumer, $token); + } + + // Internals from here + /** + * version 1 + */ + private function get_version(&$request) { + $version = $request->get_parameter("oauth_version"); + if (!$version) { + $version = 1.0; + } + if ($version && $version != $this->version) { + throw new OAuthException("OAuth version '$version' not supported"); + } + return $version; + } + + /** + * figure out the signature with some defaults + */ + private function get_signature_method(&$request) { + $signature_method = + @$request->get_parameter("oauth_signature_method"); + if (!$signature_method) { + $signature_method = "PLAINTEXT"; + } + if (!in_array($signature_method, + array_keys($this->signature_methods))) { + throw new OAuthException( + "Signature method '$signature_method' not supported " . + "try one of the following: " . + implode(", ", array_keys($this->signature_methods)) + ); + } + return $this->signature_methods[$signature_method]; + } + + /** + * try to find the consumer for the provided request's consumer key + */ + private function get_consumer(&$request) { + $consumer_key = @$request->get_parameter("oauth_consumer_key"); + if (!$consumer_key) { + throw new OAuthException("Invalid consumer key"); + } + + $consumer = $this->data_store->lookup_consumer($consumer_key); + if (!$consumer) { + throw new OAuthException("Invalid consumer"); + } + + return $consumer; + } + + /** + * try to find the token for the provided request's token key + */ + private function get_token(&$request, $consumer, $token_type="access") { + $token_field = @$request->get_parameter('oauth_token'); + if ( !$token_field) return false; + $token = $this->data_store->lookup_token( + $consumer, $token_type, $token_field + ); + if (!$token) { + throw new OAuthException("Invalid $token_type token: $token_field"); + } + return $token; + } + + /** + * all-in-one function to check the signature on a request + * should guess the signature method appropriately + */ + private function check_signature(&$request, $consumer, $token) { + // this should probably be in a different method + global $OAuth_last_computed_signature; + $OAuth_last_computed_signature = false; + + $timestamp = @$request->get_parameter('oauth_timestamp'); + $nonce = @$request->get_parameter('oauth_nonce'); + + $this->check_timestamp($timestamp); + $this->check_nonce($consumer, $token, $nonce, $timestamp); + + $signature_method = $this->get_signature_method($request); + + $signature = $request->get_parameter('oauth_signature'); + $valid_sig = $signature_method->check_signature( + $request, + $consumer, + $token, + $signature + ); + + if (!$valid_sig) { + $ex_text = "Invalid signature"; + if ( $OAuth_last_computed_signature ) { + $ex_text = $ex_text . " ours= $OAuth_last_computed_signature yours=$signature"; + } + throw new OAuthException($ex_text); + } + } + + /** + * check that the timestamp is new enough + */ + private function check_timestamp($timestamp) { + // verify that timestamp is recentish + $now = time(); + if ($now - $timestamp > $this->timestamp_threshold) { + throw new OAuthException( + "Expired timestamp, yours $timestamp, ours $now" + ); + } + } + + /** + * check that the nonce is not repeated + */ + private function check_nonce($consumer, $token, $nonce, $timestamp) { + // verify that the nonce is uniqueish + $found = $this->data_store->lookup_nonce( + $consumer, + $token, + $nonce, + $timestamp + ); + if ($found) { + throw new OAuthException("Nonce already used: $nonce"); + } + } + +} + +class OAuthDataStore { + function lookup_consumer($consumer_key) { + // implement me + } + + function lookup_token($consumer, $token_type, $token) { + // implement me + } + + function lookup_nonce($consumer, $token, $nonce, $timestamp) { + // implement me + } + + function new_request_token($consumer) { + // return a new token attached to this consumer + } + + function new_access_token($token, $consumer) { + // return a new access token attached to this consumer + // for the user associated with this token if the request token + // is authorized + // should also invalidate the request token + } + +} + +class OAuthUtil { + public static function urlencode_rfc3986($input) { + if (is_array($input)) { + return array_map(array('OAuthUtil', 'urlencode_rfc3986'), $input); + } else if (is_scalar($input)) { + return str_replace( + '+', + ' ', + str_replace('%7E', '~', rawurlencode($input)) + ); + } else { + return ''; + } +} + + + // This decode function isn't taking into consideration the above + // modifications to the encoding process. However, this method doesn't + // seem to be used anywhere so leaving it as is. + public static function urldecode_rfc3986($string) { + return urldecode($string); + } + + // Utility function for turning the Authorization: header into + // parameters, has to do some unescaping + // Can filter out any non-oauth parameters if needed (default behaviour) + public static function split_header($header, $only_allow_oauth_parameters = true) { + $pattern = '/(([-_a-z]*)=("([^"]*)"|([^,]*)),?)/'; + $offset = 0; + $params = array(); + while (preg_match($pattern, $header, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) { + $match = $matches[0]; + $header_name = $matches[2][0]; + $header_content = (isset($matches[5])) ? $matches[5][0] : $matches[4][0]; + if (preg_match('/^oauth_/', $header_name) || !$only_allow_oauth_parameters) { + $params[$header_name] = OAuthUtil::urldecode_rfc3986($header_content); + } + $offset = $match[1] + strlen($match[0]); + } + + if (isset($params['realm'])) { + unset($params['realm']); + } + + return $params; + } + + // helper to try to sort out headers for people who aren't running apache + public static function get_headers() { + if (function_exists('apache_request_headers')) { + // we need this to get the actual Authorization: header + // because apache tends to tell us it doesn't exist + return apache_request_headers(); + } + // otherwise we don't have apache and are just going to have to hope + // that $_SERVER actually contains what we need + $out = array(); + foreach ($_SERVER as $key => $value) { + if (substr($key, 0, 5) == "HTTP_") { + // this is chaos, basically it is just there to capitalize the first + // letter of every word that is not an initial HTTP and strip HTTP + // code from przemek + $key = str_replace( + " ", + "-", + ucwords(strtolower(str_replace("_", " ", substr($key, 5)))) + ); + $out[$key] = $value; + } + } + return $out; + } + + // This function takes a input like a=b&a=c&d=e and returns the parsed + // parameters like this + // array('a' => array('b','c'), 'd' => 'e') + public static function parse_parameters( $input ) { + if (!isset($input) || !$input) return array(); + + $pairs = explode('&', $input); + + $parsed_parameters = array(); + foreach ($pairs as $pair) { + $split = explode('=', $pair, 2); + $parameter = OAuthUtil::urldecode_rfc3986($split[0]); + $value = isset($split[1]) ? OAuthUtil::urldecode_rfc3986($split[1]) : ''; + + if (isset($parsed_parameters[$parameter])) { + // We have already recieved parameter(s) with this name, so add to the list + // of parameters with this name + + if (is_scalar($parsed_parameters[$parameter])) { + // This is the first duplicate, so transform scalar (string) into an array + // so we can add the duplicates + $parsed_parameters[$parameter] = array($parsed_parameters[$parameter]); + } + + $parsed_parameters[$parameter][] = $value; + } else { + $parsed_parameters[$parameter] = $value; + } + } + return $parsed_parameters; + } + + public static function build_http_query($params) { + if (!$params) return ''; + + // Urlencode both keys and values + $keys = OAuthUtil::urlencode_rfc3986(array_keys($params)); + $values = OAuthUtil::urlencode_rfc3986(array_values($params)); + $params = array_combine($keys, $values); + + // Parameters are sorted by name, using lexicographical byte value ordering. + // Ref: Spec: 9.1.1 (1) + uksort($params, 'strcmp'); + + $pairs = array(); + foreach ($params as $parameter => $value) { + if (is_array($value)) { + // If two or more parameters share the same name, they are sorted by their value + // Ref: Spec: 9.1.1 (1) + natsort($value); + foreach ($value as $duplicate_value) { + $pairs[] = $parameter . '=' . $duplicate_value; + } + } else { + $pairs[] = $parameter . '=' . $value; + } + } + // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61) + // Each name-value pair is separated by an '&' character (ASCII code 38) + return implode('&', $pairs); + } +} + +?> diff --git a/enrol/lti/ims-blti/OAuthBody.php b/enrol/lti/ims-blti/OAuthBody.php new file mode 100644 index 00000000000..be7b7f9b27d --- /dev/null +++ b/enrol/lti/ims-blti/OAuthBody.php @@ -0,0 +1,146 @@ +add_consumer($oauth_consumer_key, $oauth_consumer_secret); + + $server = new OAuthServer($store); + + $method = new OAuthSignatureMethod_HMAC_SHA1(); + $server->add_signature_method($method); + $request = OAuthRequest::from_request(); + + global $LastOAuthBodyBaseString; + $LastOAuthBodyBaseString = $request->get_signature_base_string(); + // echo($LastOAuthBodyBaseString."\n"); + + try { + $server->verify_request($request); + } catch (Exception $e) { + $message = $e->getMessage(); + throw new Exception("OAuth signature failed: " . $message); + } + + $postdata = file_get_contents('php://input'); + // echo($postdata); + + $hash = base64_encode(sha1($postdata, TRUE)); + + if ( $hash != $oauth_body_hash ) { + throw new Exception("OAuth oauth_body_hash mismatch"); + } + + return $postdata; +} + +function sendOAuthBodyPOST($method, $endpoint, $oauth_consumer_key, $oauth_consumer_secret, $content_type, $body) +{ + global $CFG; + + require_once($CFG->dirroot . '/lib/filelib.php'); + + $hash = base64_encode(sha1($body, TRUE)); + + $parms = array('oauth_body_hash' => $hash); + + $test_token = ''; + $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); + $test_consumer = new OAuthConsumer($oauth_consumer_key, $oauth_consumer_secret, NULL); + + $acc_req = OAuthRequest::from_consumer_and_token($test_consumer, $test_token, $method, $endpoint, $parms); + $acc_req->sign_request($hmac_method, $test_consumer, $test_token); + + // Pass this back up "out of band" for debugging + global $LastOAuthBodyBaseString; + $LastOAuthBodyBaseString = $acc_req->get_signature_base_string(); + // echo($LastOAuthBodyBaseString."\m"); + + $headers = array(); + $headers[] = $acc_req->to_header(); + $headers[] = "Content-type: " . $content_type; + + $curl = new curl(); + $curl->setHeader($headers); + $response = $curl->post($endpoint, $body); + + return $response; +} + +function sendOAuthParamsPOST($method, $endpoint, $oauth_consumer_key, $oauth_consumer_secret, $content_type, $params) +{ + + if (is_array($params)) { + $body = http_build_query($params, '', '&'); + } else { + $body = $params; + } + + $hash = base64_encode(sha1($body, TRUE)); + + $parms = $params; + $parms['oauth_body_hash'] = $hash; + + $test_token = ''; + $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); + $test_consumer = new OAuthConsumer($oauth_consumer_key, $oauth_consumer_secret, NULL); + + $acc_req = OAuthRequest::from_consumer_and_token($test_consumer, $test_token, $method, $endpoint, $parms); + $acc_req->sign_request($hmac_method, $test_consumer, $test_token); + + // Pass this back up "out of band" for debugging + global $LastOAuthBodyBaseString; + $LastOAuthBodyBaseString = $acc_req->get_signature_base_string(); + // echo($LastOAuthBodyBaseString."\m"); + + $header = $acc_req->to_header(); + $header = $header . "\r\nContent-type: " . $content_type . "\r\n"; + + $params = array('http' => array( + 'method' => 'POST', + 'content' => $body, + 'header' => $header + )); + $ctx = stream_context_create($params); + $fp = @fopen($endpoint, 'rb', false, $ctx); + if (!$fp) { + throw new \Exception("Problem with $endpoint, $php_errormsg"); + } + $response = @stream_get_contents($fp); + if ($response === false) { + throw new \Exception("Problem reading data from $endpoint, $php_errormsg"); + } + return $response; +} + +?> diff --git a/enrol/lti/ims-blti/TrivialOAuthDataStore.php b/enrol/lti/ims-blti/TrivialOAuthDataStore.php new file mode 100644 index 00000000000..e283eed3360 --- /dev/null +++ b/enrol/lti/ims-blti/TrivialOAuthDataStore.php @@ -0,0 +1,48 @@ +dirroot . '/enrol/lti/ims-blti/OAuth.php'); + +/** + * A Trivial memory-based store - no support for tokens + */ +class TrivialOAuthDataStore extends OAuthDataStore { + private $consumers = array(); + + function add_consumer($consumer_key, $consumer_secret) { + $this->consumers[$consumer_key] = $consumer_secret; + } + + function lookup_consumer($consumer_key) { + if ( strpos($consumer_key, "http://" ) === 0 ) { + $consumer = new OAuthConsumer($consumer_key,"secret", NULL); + return $consumer; + } + if ( $this->consumers[$consumer_key] ) { + $consumer = new OAuthConsumer($consumer_key,$this->consumers[$consumer_key], NULL); + return $consumer; + } + return NULL; + } + + function lookup_token($consumer, $token_type, $token) { + return new OAuthToken($consumer, ""); + } + + // Return NULL if the nonce has not been used + // Return $nonce if the nonce was previously used + function lookup_nonce($consumer, $token, $nonce, $timestamp) { + // Should add some clever logic to keep nonces from + // being reused - for no we are really trusting + // that the timestamp will save us + return NULL; + } + + function new_request_token($consumer) { + return NULL; + } + + function new_access_token($token, $consumer) { + return NULL; + } +} +?> \ No newline at end of file diff --git a/enrol/lti/ims-blti/blti.php b/enrol/lti/ims-blti/blti.php new file mode 100644 index 00000000000..5050183ee9e --- /dev/null +++ b/enrol/lti/ims-blti/blti.php @@ -0,0 +1,277 @@ +dirroot . '/enrol/lti/ims-blti/OAuth.php'); +require_once($CFG->dirroot . '/enrol/lti/ims-blti/TrivialOAuthDataStore.php'); + +// Returns true if this is a Basic LTI message +// with minimum values to meet the protocol +function is_basic_lti_request() { + $good_message_type = $_REQUEST["lti_message_type"] == "basic-lti-launch-request"; + $good_lti_version = ($_REQUEST["lti_version"] == "LTI-1p0" or $_REQUEST["lti_version"] == "LTI-1.0"); + $resource_link_id = $_REQUEST["resource_link_id"]; + if ($good_message_type and $good_lti_version and isset($resource_link_id) ) return(true); + return false; +} + +// Basic LTI Class that does the setup and provides utility +// functions +class BLTI { + + public $valid = false; + public $complete = false; + public $message = false; + public $basestring = false; + public $info = false; + public $row = false; + public $context_id = false; // Override context_id + + function __construct($parm=false, $usesession=true, $doredirect=true) { + + + // If this request is not an LTI Launch, either + // give up or try to retrieve the context from session + if ( ! is_basic_lti_request() ) { + + if ( $usesession === false ) return; + + if ( strlen(session_id()) > 0 ) { + $row = $_SESSION['_basiclti_lti_row']; + if ( isset($row) ) $this->row = $row; + $context_id = $_SESSION['_basiclti_lti_context_id']; + if ( isset($context_id) ) $this->context_id = $context_id; + $info = $_SESSION['_basic_lti_context']; + if ( isset($info) ) { + $this->info = $info; + $this->valid = true; + return; + } + $this->message = "Could not find context in session"; + return; + } + $this->message = "Session not available"; + return; + } + // Insure we have a valid launch + if ( empty($_REQUEST["oauth_consumer_key"]) ) { + $this->message = "Missing oauth_consumer_key in request"; + return; + } + $oauth_consumer_key = $_REQUEST["oauth_consumer_key"]; + + // Find the secret - either form the parameter as a string or + // look it up in a database from parameters we are given + $secret = false; + $row = false; + if ( is_string($parm) ) { + $secret = $parm; + } else if ( ! is_array($parm) ) { + $this->message = "Constructor requires a secret or database information."; + return; + } + + // Verify the message signature + $store = new TrivialOAuthDataStore(); + $store->add_consumer($oauth_consumer_key, $secret); + + $server = new OAuthServer($store); + + $method = new OAuthSignatureMethod_HMAC_SHA1(); + $server->add_signature_method($method); + $request = OAuthRequest::from_request(); + + $this->basestring = $request->get_signature_base_string(); + try { + $server->verify_request($request); + $this->valid = true; + } catch (Exception $e) { + $this->message = $e->getMessage(); + return; + } + // Store the launch information in the session for later + $newinfo = array(); + foreach($_POST as $key => $value ) { + if ( $key == "basiclti_submit" ) continue; + if ( strpos($key, "oauth_") === false ) { + $newinfo[$key] = $value; + continue; + } + if ( $key == "oauth_consumer_key" ) { + $newinfo[$key] = $value; + continue; + } + } + //Added abertranb to decode base 64 20120801 + if (isset($newinfo['custom_lti_message_encoded_base64']) && $newinfo['custom_lti_message_encoded_base64']==1){ + $newinfo = $this->decodeBase64($newinfo); + } + + $this->info = $newinfo; + + if ( $usesession == true and strlen(session_id()) > 0 ) { + $_SESSION['_basic_lti_context'] = $this->info; + unset($_SESSION['_basiclti_lti_row']); + unset($_SESSION['_basiclti_lti_context_id']); + if ( $this->row ) $_SESSION['_basiclti_lti_row'] = $this->row; + if ( $this->context_id ) $_SESSION['_basiclti_lti_context_id'] = $this->context_id; + } + + if ( $this->valid && $doredirect ) { + $this->redirect(); + $this->complete = true; + } + } + + function addSession($location) { + if ( ini_get('session.use_cookies') == 0 ) { + if ( strpos($location,'?') > 0 ) { + $location = $location . '&'; + } else { + $location = $location . '?'; + } + $location = $location . session_name() . '=' . session_id(); + } + return $location; + } + + function isInstructor() { + $roles = $this->info['roles']; + $roles = strtolower($roles); + if ( ! ( strpos($roles,"instructor") === false ) ) return true; + if ( ! ( strpos($roles,"administrator") === false ) ) return true; + return false; + } + + function getUserEmail() { + # set default email in the event privacy settings don't pass in email. + $email = $this->info['user_id'] . "@ltiuser.com"; + if ( isset($this->info['lis_person_contact_email_primary']) ) $email = $this->info['lis_person_contact_email_primary']; + # Sakai Hack + if ( isset($this->info['lis_person_contact_emailprimary']) ) $email = $this->info['lis_person_contact_emailprimary']; + return $email; + } + + function getUserShortName() { + $email = $this->getUserEmail(); + $givenname = $this->info['lis_person_name_given']; + $familyname = $this->info['lis_person_name_family']; + $fullname = $this->info['lis_person_name_full']; + if ( strlen($email) > 0 ) return $email; + if ( strlen($givenname) > 0 ) return $givenname; + if ( strlen($familyname) > 0 ) return $familyname; + return $this->getUserName(); + } + + function getUserName() { + $givenname = $this->info['lis_person_name_given']; + $familyname = $this->info['lis_person_name_family']; + $fullname = $this->info['lis_person_name_full']; + if ( strlen($fullname) > 0 ) return $fullname; + if ( strlen($familyname) > 0 and strlen($givenname) > 0 ) return $givenname + $familyname; + if ( strlen($givenname) > 0 ) return $givenname; + if ( strlen($familyname) > 0 ) return $familyname; + return $this->getUserEmail(); + } + + function getUserKey() { + $oauth = $this->info['oauth_consumer_key']; + $id = $this->info['user_id']; + if ( strlen($id) > 0 and strlen($oauth) > 0 ) return $oauth . ':' . $id; + return false; + } + + function getUserImage() { + $image = $this->info['user_image']; + if ( strlen($image) > 0 ) return $image; + $email = $this->getUserEmail(); + if ( $email === false ) return false; + $size = 40; + $grav_url = $_SERVER['HTTPS'] ? 'https://' : 'http://'; + $grav_url = $grav_url . "www.gravatar.com/avatar.php?gravatar_id=".md5( strtolower($email) )."&size=".$size; + return $grav_url; + } + + function getResourceKey() { + $oauth = $this->info['oauth_consumer_key']; + $id = $this->info['resource_link_id']; + if ( strlen($id) > 0 and strlen($oauth) > 0 ) return $oauth . ':' . $id; + return false; + } + + function getResourceTitle() { + $title = $this->info['resource_link_title']; + if ( strlen($title) > 0 ) return $title; + return false; + } + + function getConsumerKey() { + $oauth = $this->info['oauth_consumer_key']; + return $oauth; + } + + function getCourseKey() { + if ( $this->context_id ) return $this->context_id; + $oauth = $this->info['oauth_consumer_key']; + $id = $this->info['context_id']; + if ( strlen($id) > 0 and strlen($oauth) > 0 ) return $oauth . ':' . $id; + return false; + } + + function getCourseName() { + $label = $this->info['context_label']; + $title = $this->info['context_title']; + $id = $this->info['context_id']; + if ( strlen($label) > 0 ) return $label; + if ( strlen($title) > 0 ) return $title; + if ( strlen($id) > 0 ) return $id; + return false; + } + + // TODO: Add javasript version if headers are already sent + function redirect() { + $host = $_SERVER['HTTP_HOST']; + $uri = $_SERVER['PHP_SELF']; + $location = $_SERVER['HTTPS'] ? 'https://' : 'http://'; + $location = $location . $host . $uri; + $location = $this->addSession($location); + header("Location: $location"); + } + + function dump() { + if ( ! $this->valid or $this->info == false ) return "Context not valid\n"; + $ret = ""; + if ( $this->isInstructor() ) { + $ret .= "isInstructor() = true\n"; + } else { + $ret .= "isInstructor() = false\n"; + } + $ret .= "getUserKey() = ".$this->getUserKey()."\n"; + $ret .= "getUserEmail() = ".$this->getUserEmail()."\n"; + $ret .= "getUserShortName() = ".$this->getUserShortName()."\n"; + $ret .= "getUserName() = ".$this->getUserName()."\n"; + $ret .= "getUserImage() = ".$this->getUserImage()."\n"; + $ret .= "getResourceKey() = ".$this->getResourceKey()."\n"; + $ret .= "getResourceTitle() = ".$this->getResourceTitle()."\n"; + $ret .= "getCourseName() = ".$this->getCourseName()."\n"; + $ret .= "getCourseKey() = ".$this->getCourseKey()."\n"; + $ret .= "getConsumerKey() = ".$this->getConsumerKey()."\n"; + return $ret; + } + + /** + * Data submitter are in base64 then we have to decode + * @author Antoni Bertran (antoni@tresipunt.com) + * @param $info array + * @date 20120801 + */ + function decodeBase64($info) { + $keysNoEncode = array("lti_version", "lti_message_type", "tool_consumer_instance_description", "tool_consumer_instance_guid", "oauth_consumer_key", "custom_lti_message_encoded_base64", "oauth_nonce", "oauth_version", "oauth_callback", "oauth_timestamp", "basiclti_submit", "oauth_signature_method", "ext_ims_lis_memberships_id", "ext_ims_lis_memberships_url"); + foreach ($info as $key => $item){ + if (!in_array($key, $keysNoEncode)) + $info[$key] = base64_decode($item); + } + return $info; + } + +} + +?> diff --git a/enrol/lti/ims-blti/blti_util.php b/enrol/lti/ims-blti/blti_util.php new file mode 100644 index 00000000000..850131fe98c --- /dev/null +++ b/enrol/lti/ims-blti/blti_util.php @@ -0,0 +1,219 @@ + "120988f929-274612", + "resource_link_title" => "Weekly Blog", + "resource_link_description" => "Each student needs to reflect on the weekly reading. These should be one paragraph long.", + "user_id" => "292832126", + "roles" => "Instructor", // or Learner + "lis_person_name_full" => 'Jane Q. Public', + "lis_person_contact_email_primary" => "user@school.edu", + "lis_person_sourcedid" => "school.edu:user", + "context_id" => "456434513", + "context_title" => "Design of Personal Environments", + "context_label" => "SI182", + ); + + return $parms; + } + + function validateDescriptor($descriptor) + { + $xml = new SimpleXMLElement($xmldata); + if ( ! $xml ) { + echo("Error parsing Descriptor XML\n"); + return; + } + $launch_url = $xml->secure_launch_url[0]; + if ( ! $launch_url ) $launch_url = $xml->launch_url[0]; + if ( $launch_url ) $launch_url = (string) $launch_url; + return $launch_url; + } + + // Parse a descriptor + function launchInfo($xmldata) { + $xml = new SimpleXMLElement($xmldata); + if ( ! $xml ) { + echo("Error parsing Descriptor XML\n"); + return; + } + $launch_url = $xml->secure_launch_url[0]; + if ( ! $launch_url ) $launch_url = $xml->launch_url[0]; + if ( $launch_url ) $launch_url = (string) $launch_url; + $custom = array(); + if ( $xml->custom[0]->parameter ) + foreach ( $xml->custom[0]->parameter as $resource) { + $key = (string) $resource['key']; + $key = strtolower($key); + $nk = ""; + for($i=0; $i < strlen($key); $i++) { + $ch = substr($key,$i,1); + if ( $ch >= "a" && $ch <= "z" ) $nk .= $ch; + else if ( $ch >= "0" && $ch <= "9" ) $nk .= $ch; + else $nk .= "_"; + } + $value = (string) $resource; + $custom["custom_".$nk] = $value; + } + return array("launch_url" => $launch_url, "custom" => $custom ) ; + } + +function signParameters($oldparms, $endpoint, $method, $oauth_consumer_key, $oauth_consumer_secret, + $submit_text = false, $org_id = false, $org_desc = false) +{ + global $last_base_string; + $parms = $oldparms; + if ( ! isset($parms["lti_version"]) ) $parms["lti_version"] = "LTI-1p0"; + if ( ! isset($parms["lti_message_type"]) ) $parms["lti_message_type"] = "basic-lti-launch-request"; + if ( ! isset($parms["oauth_callback"]) ) $parms["oauth_callback"] = "about:blank"; + if ( $org_id ) $parms["tool_consumer_instance_guid"] = $org_id; + if ( $org_desc ) $parms["tool_consumer_instance_description"] = $org_desc; + if ( $submit_text ) $parms["ext_submit"] = $submit_text; + + $test_token = ''; + + $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); + $test_consumer = new OAuthConsumer($oauth_consumer_key, $oauth_consumer_secret, NULL); + + $acc_req = OAuthRequest::from_consumer_and_token($test_consumer, $test_token, $method, $endpoint, $parms); + + $acc_req->sign_request($hmac_method, $test_consumer, $test_token); + + // Pass this back up "out of band" for debugging + $last_base_string = $acc_req->get_signature_base_string(); + + $newparms = $acc_req->get_parameters(); + + return $newparms; +} + +function signOnly($oldparms, $endpoint, $method, $oauth_consumer_key, $oauth_consumer_secret) +{ + global $last_base_string; + $parms = $oldparms; + + $test_token = ''; + + $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); + $test_consumer = new OAuthConsumer($oauth_consumer_key, $oauth_consumer_secret, NULL); + + $acc_req = OAuthRequest::from_consumer_and_token($test_consumer, $test_token, $method, $endpoint, $parms); + $acc_req->sign_request($hmac_method, $test_consumer, $test_token); + + // Pass this back up "out of band" for debugging + $last_base_string = $acc_req->get_signature_base_string(); + + $newparms = $acc_req->get_parameters(); + + return $newparms; +} + +function postLaunchHTML($newparms, $endpoint, $debug=false, $iframeattr=false) { + global $last_base_string; + $r = "
\n"; + if ( $iframeattr ) { + $r = "
\n" ; + } else { + $r = "\n" ; + } + $submit_text = $newparms['ext_submit']; + foreach($newparms as $key => $value ) { + $key = htmlspecialchars($key); + $value = htmlspecialchars($value); + if ( $key == "ext_submit" ) { + $r .= "\n"; + } + if ( $debug ) { + $r .= "\n"; + $r .= ""; + $r .= get_stringIMS("toggle_debug_data","basiclti")."\n"; + $r .= "
\n"; + $r .= "".get_stringIMS("basiclti_endpoint","basiclti")."
\n"; + $r .= $endpoint . "
\n 
\n"; + $r .= "".get_stringIMS("basiclti_parameters","basiclti")."
\n"; + foreach($newparms as $key => $value ) { + $key = htmlspecialchars($key); + $value = htmlspecialchars($value); + $r .= "$key = $value
\n"; + } + $r .= " 
\n"; + $r .= "

".get_stringIMS("basiclti_base_string","basiclti")."
\n".$last_base_string."

\n"; + $r .= "
\n"; + } + $r .= "
\n"; + if ( $iframeattr ) { + $r .= "\n"; + } + if ( ! $debug ) { + $ext_submit = "ext_submit"; + $ext_submit_text = $submit_text; + $r .= " \n"; + } + $r .= "
\n"; + return $r; +} + +/* This is a bit of homage to Moodle's pattern of internationalisation */ +function get_stringIMS($key,$bundle) { + return $key; +} + +function do_post_request($url, $data, $optional_headers = null) +{ + $params = array('http' => array( + 'method' => 'POST', + 'content' => $data + )); + + if ($optional_headers !== null) { + $header = $optional_headers . "\r\n"; + } + // $header = $header . "Content-type: application/x-www-form-urlencoded\r\n"; + $params['http']['header'] = $header; + $ctx = stream_context_create($params); + $fp = @fopen($url, 'rb', false, $ctx); + if (!$fp) { + echo @stream_get_contents($fp); + throw new Exception("Problem with $url, $php_errormsg"); + } + $response = @stream_get_contents($fp); + if ($response === false) { + throw new Exception("Problem reading data from $url, $php_errormsg"); + } + return $response; +} + diff --git a/enrol/lti/ims-blti/moodle_readme.txt b/enrol/lti/ims-blti/moodle_readme.txt new file mode 100644 index 00000000000..eb7c4f28330 --- /dev/null +++ b/enrol/lti/ims-blti/moodle_readme.txt @@ -0,0 +1,4 @@ +This library was originally published by the IMS at https://code.google.com/p/ims-dev/ which no longer exists. The +current code was taken from https://github.com/jfederico/ims-dev/tree/master/basiclti/php-simple/ims-blti - with +several changes to the code (including bug fixes). As the library is no longer supported upgrades are not possible. +In future releases we should look into using a supported library. diff --git a/enrol/lti/index.php b/enrol/lti/index.php new file mode 100644 index 00000000000..3640f864d2e --- /dev/null +++ b/enrol/lti/index.php @@ -0,0 +1,123 @@ +. + +/** + * List the tool provided in a course + * + * @package enrol_lti + * @copyright 2016 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__) . '/../../config.php'); +require_once($CFG->dirroot.'/enrol/lti/lib.php'); + +$courseid = required_param('courseid', PARAM_INT); +$action = optional_param('action', '', PARAM_ALPHA); +if ($action) { + require_sesskey(); + $instanceid = required_param('instanceid', PARAM_INT); + $instance = $DB->get_record('enrol', array('id' => $instanceid), '*', MUST_EXIST); +} +$confirm = optional_param('confirm', 0, PARAM_INT); + +$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); + +$context = context_course::instance($course->id); + +require_login($course); +require_capability('moodle/course:enrolreview', $context); + +$ltiplugin = enrol_get_plugin('lti'); +$canconfig = has_capability('moodle/course:enrolconfig', $context); +$pageurl = new moodle_url('/enrol/lti/index.php', array('courseid' => $courseid)); + +$PAGE->set_url($pageurl); +$PAGE->set_title(get_string('course') . ': ' . $course->fullname); +$PAGE->set_pagelayout('admin'); + +// Check if we want to perform any actions. +if ($action) { + if ($action === 'delete') { + if ($ltiplugin->can_delete_instance($instance)) { + if ($confirm) { + $ltiplugin->delete_instance($instance); + redirect($PAGE->url); + } + + $yesurl = new moodle_url('/enrol/lti/index.php', + array('courseid' => $course->id, + 'action' => 'delete', + 'instanceid' => $instance->id, + 'confirm' => 1, + 'sesskey' => sesskey()) + ); + $displayname = $ltiplugin->get_instance_name($instance); + $users = $DB->count_records('user_enrolments', array('enrolid' => $instance->id)); + if ($users) { + $message = markdown_to_html(get_string('deleteinstanceconfirm', 'enrol', + array('name' => $displayname, + 'users' => $users))); + } else { + $message = markdown_to_html(get_string('deleteinstancenousersconfirm', 'enrol', + array('name' => $displayname))); + } + echo $OUTPUT->header(); + echo $OUTPUT->confirm($message, $yesurl, $PAGE->url); + echo $OUTPUT->footer(); + die(); + } + } else if ($action === 'disable') { + if ($ltiplugin->can_hide_show_instance($instance)) { + if ($instance->status != ENROL_INSTANCE_DISABLED) { + $ltiplugin->update_status($instance, ENROL_INSTANCE_DISABLED); + redirect($PAGE->url); + } + } + } else if ($action === 'enable') { + if ($ltiplugin->can_hide_show_instance($instance)) { + if ($instance->status != ENROL_INSTANCE_ENABLED) { + $ltiplugin->update_status($instance, ENROL_INSTANCE_ENABLED); + redirect($PAGE->url); + } + } + } +} + +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('toolsprovided', 'enrol_lti')); + +if (\enrol_lti\helper::count_lti_tools(array('courseid' => $courseid)) > 0) { + $table = new \enrol_lti\manage_table($courseid); + $table->define_baseurl($pageurl); + $table->out(50, false); +} else { + $notify = new \core\output\notification(get_string('notoolsprovided', 'enrol_lti'), + \core\output\notification::NOTIFY_WARNING); + echo $OUTPUT->render($notify); +} + +if ($ltiplugin->can_add_instance($course->id)) { + echo $OUTPUT->single_button(new moodle_url('/enrol/editinstance.php', + array( + 'type' => 'lti', + 'courseid' => $course->id, + 'returnurl' => new moodle_url('/enrol/lti/index.php', array('courseid' => $course->id))) + ), + get_string('add')); +} + +echo $OUTPUT->footer(); diff --git a/enrol/lti/lang/en/enrol_lti.php b/enrol/lti/lang/en/enrol_lti.php new file mode 100644 index 00000000000..f3366464bbc --- /dev/null +++ b/enrol/lti/lang/en/enrol_lti.php @@ -0,0 +1,66 @@ +. + +/** + * LTI enrolment plugin version information + * + * @package enrol_lti + * @copyright 2016 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['enrolenddate'] = 'End date'; +$string['enrolenddate_help'] = 'If enabled, users can access until this date only.'; +$string['enrolenddateerror'] = 'Enrolment end date cannot be earlier than start date'; +$string['enrolisdisabled'] = 'The LTI enrolment plugin is disabled.'; +$string['enrolperiod'] = 'Enrolment duration'; +$string['enrolperiod_help'] = 'Length of time that the enrolment is valid, starting with the moment the user enrols themselves from the remote system. If disabled, the enrolment duration will be unlimited.'; +$string['enrolmentfinished'] = 'Enrolment finished.'; +$string['enrolmentnotstarted'] = 'Enrolment has not started.'; +$string['enrolstartdate'] = 'Start date'; +$string['enrolstartdate_help'] = 'If enabled, users can access from this date onward only.'; +$string['globalsharedsecret'] = 'Global shared secret'; +$string['gradesync'] = 'Grade synchronisation'; +$string['gradesync_help'] = 'This determines if we want grade synchronisation to occur.'; +$string['maxenrolled'] = 'Maximum enrolled'; +$string['maxenrolled_help'] = 'Specifies the maximum number of users that can access from the remote system. The value \'0\' means there is no limit.'; +$string['maxenrolledreached'] = 'Maximum number of users allowed to access was already reached.'; +$string['membersync'] = 'Member synchronisation'; +$string['membersync_help'] = 'This determines if we want member synchronisation to occur.'; +$string['membersyncmode'] = 'Members synchronisation mode'; +$string['membersyncmode_help'] = 'This setting determines what we should do when synchronising members.'; +$string['membersyncmodeenrolandunenrol'] = 'Enrol new and unenrol missing members'; +$string['membersyncmodeenrolnew'] = 'Enrol new members'; +$string['membersyncmodeunenrolmissing'] = 'Unenrol missing members'; +$string['notoolsprovided'] = 'No tools provided'; +$string['lti:config'] = 'Configure LTI enrol instances'; +$string['lti:unenrol'] = 'Unenrol users from the course'; +$string['pluginname'] = 'Shared external tool'; +$string['pluginname_desc'] = 'The shared external tool plugin allows externals users to access a course or an activity via a unique link - this requires the LTI authentication plugin to be enabled.'; +$string['remotesystem'] = 'Remote system'; +$string['requirecompletion'] = 'Require the course or activity to be completed before sending the grades'; +$string['roleinstructor'] = 'Role for instructor'; +$string['roleinstructor_help'] = 'This is the role that will be assigned at the context of the tool specificed to LTI consumer instructor.'; +$string['rolelearner'] = 'Role for learner'; +$string['rolelearner_help'] = 'This is the role that will be assigned at the context of the tool specificed to the LTI consumer student.'; +$string['secret'] = 'Secret'; +$string['secret_help'] = 'This is the secret that is shared with the LTI consumer in order for them to access this tool'; +$string['sharedexternaltools'] = 'Shared external tools'; +$string['syncsettings'] = 'Synchronisation settings'; +$string['tooldoesnotexist'] = 'The requested tool does not exist.'; +$string['toolsprovided'] = 'Tools provided'; +$string['tooltobeprovided'] = 'Tool to be provided'; +$string['userdefaultvalues'] = 'User default values'; diff --git a/enrol/lti/lib.php b/enrol/lti/lib.php new file mode 100644 index 00000000000..10940b68f36 --- /dev/null +++ b/enrol/lti/lib.php @@ -0,0 +1,408 @@ +. + +/** + * LTI enrolment plugin main library file. + * + * @package enrol_lti + * @copyright 2016 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * LTI enrolment plugin class. + * + * @package enrol_lti + * @copyright 2016 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class enrol_lti_plugin extends enrol_plugin { + + /** + * Return true if we can add a new instance to this course. + * + * @param int $courseid + * @return boolean + */ + public function can_add_instance($courseid) { + $context = context_course::instance($courseid, MUST_EXIST); + return has_capability('moodle/course:enrolconfig', $context) && has_capability('enrol/lti:config', $context); + } + + /** + * Is it possible to delete enrol instance via standard UI? + * + * @param object $instance + * @return bool + */ + public function can_delete_instance($instance) { + $context = context_course::instance($instance->courseid); + return has_capability('enrol/lti:config', $context); + } + + /** + * Is it possible to hide/show enrol instance via standard UI? + * + * @param stdClass $instance + * @return bool + */ + public function can_hide_show_instance($instance) { + $context = context_course::instance($instance->courseid); + return has_capability('enrol/lti:config', $context); + } + + /** + * Returns true if it's possible to unenrol users. + * + * @param stdClass $instance course enrol instance + * @return bool + */ + public function allow_unenrol(stdClass $instance) { + return true; + } + + /** + * We are a good plugin and don't invent our own UI/validation code path. + * + * @return boolean + */ + public function use_standard_editing_ui() { + return true; + } + + /** + * Add new instance of enrol plugin. + * + * @param object $course + * @param array $fields instance fields + * @return int id of new instance, null if can not be created + */ + public function add_instance($course, array $fields = null) { + global $DB; + + $instanceid = parent::add_instance($course, $fields); + + // Add additional data to our table. + $data = new stdClass(); + $data->enrolid = $instanceid; + $data->timecreated = time(); + $data->timemodified = $data->timecreated; + foreach ($fields as $field => $value) { + $data->$field = $value; + } + + $DB->insert_record('enrol_lti_tools', $data); + + return $instanceid; + } + + /** + * Update instance of enrol plugin. + * + * @param stdClass $instance + * @param stdClass $data modified instance fields + * @return boolean + */ + public function update_instance($instance, $data) { + global $DB; + + parent::update_instance($instance, $data); + + // Remove the fields we don't want to override. + unset($data->id); + unset($data->timecreated); + unset($data->timemodified); + + // Convert to an array we can loop over. + $fields = (array) $data; + + // Update the data in our table. + $tool = new stdClass(); + $tool->id = $data->toolid; + $tool->timemodified = time(); + foreach ($fields as $field => $value) { + $tool->$field = $value; + } + + return $DB->update_record('enrol_lti_tools', $tool); + } + + /** + * Delete plugin specific information. + * + * @param stdClass $instance + * @return void + */ + public function delete_instance($instance) { + global $DB; + + // Get the tool associated with this instance. + $tool = $DB->get_record('enrol_lti_tools', array('enrolid' => $instance->id), 'id', MUST_EXIST); + + // Delete any users associated with this tool. + $DB->delete_records('enrol_lti_users', array('toolid' => $tool->id)); + + // Delete the lti tool record. + $DB->delete_records('enrol_lti_tools', array('id' => $tool->id)); + + // Time for the parent to do it's thang, yeow. + parent::delete_instance($instance); + } + + /** + * Handles un-enrolling a user. + * + * @param stdClass $instance + * @param int $userid + * @return void + */ + public function unenrol_user(stdClass $instance, $userid) { + global $DB; + + // Get the tool associated with this instance. + $tool = $DB->get_record('enrol_lti_tools', array('enrolid' => $instance->id), 'id', MUST_EXIST); + + // Need to remove the user from the users table. + $DB->delete_records('enrol_lti_users', array('userid' => $userid, 'toolid' => $tool->id)); + + parent::unenrol_user($instance, $userid); + } + + /** + * Add elements to the edit instance form. + * + * @param stdClass $instance + * @param MoodleQuickForm $mform + * @param context $context + * @return bool + */ + public function edit_instance_form($instance, MoodleQuickForm $mform, $context) { + global $DB; + + $nameattribs = array('size' => '20', 'maxlength' => '255'); + $mform->addElement('text', 'name', get_string('custominstancename', 'enrol'), $nameattribs); + $mform->setType('name', PARAM_TEXT); + $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'server'); + + $tools = array(); + $tools[$context->id] = get_string('course'); + $modinfo = get_fast_modinfo($instance->courseid); + $mods = $modinfo->get_cms(); + foreach ($mods as $mod) { + $tools[$mod->context->id] = format_string($mod->name); + } + + $mform->addElement('select', 'contextid', get_string('tooltobeprovided', 'enrol_lti'), $tools); + $mform->setDefault('contextid', $context->id); + + $mform->addElement('duration', 'enrolperiod', get_string('enrolperiod', 'enrol_lti'), + array('optional' => true, 'defaultunit' => DAYSECS)); + $mform->setDefault('enrolperiod', 0); + $mform->addHelpButton('enrolperiod', 'enrolperiod', 'enrol_lti'); + + $mform->addElement('date_time_selector', 'enrolstartdate', get_string('enrolstartdate', 'enrol_lti'), + array('optional' => true)); + $mform->setDefault('enrolstartdate', 0); + $mform->addHelpButton('enrolstartdate', 'enrolstartdate', 'enrol_lti'); + + $mform->addElement('date_time_selector', 'enrolenddate', get_string('enrolenddate', 'enrol_lti'), + array('optional' => true)); + $mform->setDefault('enrolenddate', 0); + $mform->addHelpButton('enrolenddate', 'enrolenddate', 'enrol_lti'); + + $mform->addElement('text', 'maxenrolled', get_string('maxenrolled', 'enrol_lti')); + $mform->setDefault('maxenrolled', 0); + $mform->addHelpButton('maxenrolled', 'maxenrolled', 'enrol_lti'); + $mform->setType('maxenrolled', PARAM_INT); + + $assignableroles = get_assignable_roles($context); + + $mform->addElement('select', 'roleinstructor', get_string('roleinstructor', 'enrol_lti'), $assignableroles); + $mform->setDefault('roleinstructor', '3'); + $mform->addHelpButton('roleinstructor', 'roleinstructor', 'enrol_lti'); + + $mform->addElement('select', 'rolelearner', get_string('rolelearner', 'enrol_lti'), $assignableroles); + $mform->setDefault('rolelearner', '5'); + $mform->addHelpButton('rolelearner', 'rolelearner', 'enrol_lti'); + + $mform->addElement('header', 'remotesystem', get_string('remotesystem', 'enrol_lti')); + + $mform->addElement('text', 'secret', get_string('secret', 'enrol_lti'), 'maxlength="64" size="25"'); + $mform->setType('secret', PARAM_ALPHANUM); + $mform->setDefault('secret', random_string(32)); + $mform->addHelpButton('secret', 'secret', 'enrol_lti'); + $mform->addRule('secret', get_string('required'), 'required'); + + $mform->addElement('selectyesno', 'gradesync', get_string('gradesync', 'enrol_lti')); + $mform->setDefault('gradesync', 1); + $mform->addHelpButton('gradesync', 'gradesync', 'enrol_lti'); + + $mform->addElement('selectyesno', 'gradesynccompletion', get_string('requirecompletion', 'enrol_lti')); + $mform->setDefault('gradesynccompletion', 0); + $mform->disabledIf('gradesynccompletion', 'gradesync', 0); + + $mform->addElement('selectyesno', 'membersync', get_string('membersync', 'enrol_lti')); + $mform->setDefault('membersync', 1); + $mform->addHelpButton('membersync', 'membersync', 'enrol_lti'); + + $options = array(); + $options[\enrol_lti\helper::MEMBER_SYNC_ENROL_AND_UNENROL] = get_string('membersyncmodeenrolandunenrol', 'enrol_lti'); + $options[\enrol_lti\helper::MEMBER_SYNC_ENROL_NEW] = get_string('membersyncmodeenrolnew', 'enrol_lti'); + $options[\enrol_lti\helper::MEMBER_SYNC_UNENROL_MISSING] = get_string('membersyncmodeunenrolmissing', 'enrol_lti'); + $mform->addElement('select', 'membersyncmode', get_string('membersyncmode', 'enrol_lti'), $options); + $mform->setDefault('membersyncmode', \enrol_lti\helper::MEMBER_SYNC_ENROL_AND_UNENROL); + $mform->addHelpButton('membersyncmode', 'membersyncmode', 'enrol_lti'); + $mform->disabledIf('membersyncmode', 'membersync', 0); + + $mform->addElement('header', 'defaultheader', get_string('userdefaultvalues', 'enrol_lti')); + + $emaildisplay = get_config('enrol_lti', 'emaildisplay'); + $choices = array( + 0 => get_string('emaildisplayno'), + 1 => get_string('emaildisplayyes'), + 2 => get_string('emaildisplaycourse') + ); + $mform->addElement('select', 'maildisplay', get_string('emaildisplay'), $choices); + $mform->setDefault('maildisplay', $emaildisplay); + + $city = get_config('enrol_lti', 'city'); + $mform->addElement('text', 'city', get_string('city'), 'maxlength="100" size="25"'); + $mform->setType('city', PARAM_TEXT); + $mform->setDefault('city', $city); + + $country = get_config('enrol_lti', 'country'); + $countries = array('' => get_string('selectacountry') . '...') + get_string_manager()->get_list_of_countries(); + $mform->addElement('select', 'country', get_string('selectacountry'), $countries); + $mform->setDefault('country', $country); + $mform->setAdvanced('country'); + + $timezone = get_config('enrol_lti', 'timezone'); + $choices = core_date::get_list_of_timezones(null, true); + $mform->addElement('select', 'timezone', get_string('timezone'), $choices); + $mform->setDefault('timezone', $timezone); + $mform->setAdvanced('timezone'); + + $lang = get_config('enrol_lti', 'lang'); + $mform->addElement('select', 'lang', get_string('preferredlanguage'), get_string_manager()->get_list_of_translations()); + $mform->setDefault('lang', $lang); + $mform->setAdvanced('lang'); + + $institution = get_config('enrol_lti', 'institution'); + $mform->addElement('text', 'institution', get_string('institution'), 'maxlength="40" size="25"'); + $mform->setType('institution', core_user::get_property_type('institution')); + $mform->setDefault('institution', $institution); + $mform->setAdvanced('institution'); + + // Check if we are editing an instance. + if (!empty($instance->id)) { + // Get the details from the enrol_lti_tools table. + $ltitool = $DB->get_record('enrol_lti_tools', array('enrolid' => $instance->id), '*', MUST_EXIST); + + $mform->addElement('hidden', 'toolid'); + $mform->setType('toolid', PARAM_INT); + $mform->setConstant('toolid', $ltitool->id); + + $mform->setDefaults((array) $ltitool); + } + } + + /** + * Perform custom validation of the data used to edit the instance. + * + * @param array $data array of ("fieldname"=>value) of submitted data + * @param array $files array of uploaded files "element_name"=>tmp_file_path + * @param object $instance The instance loaded from the DB + * @param context $context The context of the instance we are editing + * @return array of "element_name"=>"error_description" if there are errors, + * or an empty array if everything is OK. + * @return void + */ + public function edit_instance_validation($data, $files, $instance, $context) { + global $COURSE, $DB; + + $errors = array(); + + if (!empty($data['enrolenddate']) && $data['enrolenddate'] < $data['enrolstartdate']) { + $errors['enrolenddate'] = get_string('enrolenddateerror', 'enrol_lti'); + } + + if (!empty($data['requirecompletion'])) { + $completion = new completion_info($COURSE); + $moodlecontext = $DB->get_record('context', array('id' => $data['contextid'])); + if ($moodlecontext->contextlevel == CONTEXT_MODULE) { + $cm = get_coursemodule_from_id(false, $moodlecontext->instanceid, 0, false, MUST_EXIST); + } else { + $cm = null; + } + + if (!$completion->is_enabled($cm)) { + $errors['requirecompletion'] = get_string('errorcompletionenabled', 'enrol_lti'); + } + } + + return $errors; + } + + /** + * Gets an array of the user enrolment actions. + * + * @param course_enrolment_manager $manager + * @param stdClass $ue A user enrolment object + * @return array An array of user_enrolment_actions + */ + public function get_user_enrolment_actions(course_enrolment_manager $manager, $ue) { + $actions = array(); + $context = $manager->get_context(); + $instance = $ue->enrolmentinstance; + $params = $manager->get_moodlepage()->url->params(); + $params['ue'] = $ue->id; + if ($this->allow_unenrol_user($instance, $ue) && has_capability("enrol/lti:unenrol", $context)) { + $url = new moodle_url('/enrol/unenroluser.php', $params); + $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url, + array('class' => 'unenrollink', 'rel' => $ue->id)); + } + if ($this->allow_manage($instance) && has_capability("enrol/lti:manage", $context)) { + $url = new moodle_url('/enrol/editenrolment.php', $params); + $actions[] = new user_enrolment_action(new pix_icon('t/edit', ''), get_string('edit'), $url, + array('class' => 'editenrollink', 'rel' => $ue->id)); + } + return $actions; + } +} + +/** + * Display the LTI link in the course administration menu. + * + * @param settings_navigation $navigation The settings navigation object + * @param stdClass $course The course + * @param stdclass $context Course context + */ +function enrol_lti_extend_navigation_course($navigation, $course, $context) { + // Check that the LTI plugin is enabled. + if (enrol_is_enabled('lti')) { + // Check that they can add an instance. + $ltiplugin = enrol_get_plugin('lti'); + if ($ltiplugin->can_add_instance($course->id)) { + $url = new moodle_url('/enrol/lti/index.php', array('courseid' => $course->id)); + $settingsnode = navigation_node::create(get_string('sharedexternaltools', 'enrol_lti'), $url, + navigation_node::TYPE_SETTING, null, null, new pix_icon('i/settings', '')); + + $navigation->add_node($settingsnode); + } + } +} diff --git a/enrol/lti/settings.php b/enrol/lti/settings.php new file mode 100644 index 00000000000..7dc6db617ea --- /dev/null +++ b/enrol/lti/settings.php @@ -0,0 +1,59 @@ +. + +/** + * General plugin functions. + * + * @package enrol_lti + * @copyright 2016 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + + $settings->add(new admin_setting_heading('enrol_lti_user_default_values', + get_string('userdefaultvalues', 'enrol_lti'), '')); + + $choices = array(0 => get_string('emaildisplayno'), + 1 => get_string('emaildisplayyes'), + 2 => get_string('emaildisplaycourse')); + $settings->add(new admin_setting_configselect('enrol_lti/emaildisplay', get_string('emaildisplay'), '', + $CFG->defaultpreference_maildisplay, $choices)); + + $city = ''; + if (!empty($CFG->defaultcity)) { + $city = $CFG->defaultcity; + } + $settings->add(new admin_setting_configtext('enrol_lti/city', get_string('city'), '', $city)); + + $country = ''; + if (!empty($CFG->country)) { + $country = $CFG->country; + } + $countries = array('' => get_string('selectacountry') . '...') + get_string_manager()->get_list_of_countries(); + $settings->add(new admin_setting_configselect('enrol_lti/country', get_string('selectacountry'), '', $country, + $countries)); + + $settings->add(new admin_setting_configselect('enrol_lti/timezone', get_string('timezone'), '', 99, + core_date::get_list_of_timezones(null, true))); + + $settings->add(new admin_setting_configselect('enrol_lti/lang', get_string('preferredlanguage'), '', $CFG->lang, + get_string_manager()->get_list_of_translations())); + + $settings->add(new admin_setting_configtext('enrol_lti/institution', get_string('institution'), '', '')); +} diff --git a/enrol/lti/thirdpartylibs.xml b/enrol/lti/thirdpartylibs.xml new file mode 100644 index 00000000000..b736c2bfd70 --- /dev/null +++ b/enrol/lti/thirdpartylibs.xml @@ -0,0 +1,10 @@ + + + + ims-blti + IMS-BLTI + MIT + + + + diff --git a/enrol/lti/tool.php b/enrol/lti/tool.php new file mode 100644 index 00000000000..e2673414ff5 --- /dev/null +++ b/enrol/lti/tool.php @@ -0,0 +1,211 @@ +. + +/** + * The main entry point for the external system. + * + * @package enrol_lti + * @copyright 2016 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__) . '/../../config.php'); +require_once($CFG->dirroot . '/user/lib.php'); +require_once($CFG->dirroot . '/enrol/lti/ims-blti/blti.php'); + +$toolid = required_param('id', PARAM_INT); +$lticontextid = required_param('context_id', PARAM_RAW); + +// Get the tool. +$tool = \enrol_lti\helper::get_lti_tool($toolid); + +// Create the BLTI request. +$ltirequest = new BLTI($tool->secret, false, false); + +// Correct launch request. +if ($ltirequest->valid) { + // Check if the authentication plugin is disabled. + if (!is_enabled_auth('lti')) { + print_error('pluginnotenabled', 'auth', '', get_string('pluginname', 'auth_lti')); + exit(); + } + + // Check if the enrolment plugin is disabled. + if (!enrol_is_enabled('lti')) { + print_error('enrolisdisabled', 'enrol_lti'); + exit(); + } + + // Check if the enrolment instance is disabled. + if ($tool->status != ENROL_INSTANCE_ENABLED) { + print_error('enrolisdisabled', 'enrol_lti'); + exit(); + } + + // Before we do anything check that the context is valid. + $context = context::instance_by_id($tool->contextid); + + // Set the user data. + $user = new stdClass(); + $user->username = \enrol_lti\helper::create_username($ltirequest->info['oauth_consumer_key'], $ltirequest->info['user_id']); + if (!empty($ltirequest->info['lis_person_name_given'])) { + $user->firstname = $ltirequest->info['lis_person_name_given']; + } else { + $user->firstname = $ltirequest->info['user_id']; + } + if (!empty($ltirequest->info['lis_person_name_family'])) { + $user->lastname = $ltirequest->info['lis_person_name_family']; + } else { + $user->lastname = $ltirequest->info['context_id']; + } + + $user->email = \core_user::clean_field($ltirequest->getUserEmail(), 'email'); + + // Get the user data from the LTI consumer. + $user = \enrol_lti\helper::assign_user_tool_data($tool, $user); + + // Check if the user exists. + if (!$dbuser = $DB->get_record('user', array('username' => $user->username, 'deleted' => 0))) { + // If the email was stripped/not set then fill it with a default one. This + // stops the user from being redirected to edit their profile page. + if (empty($user->email)) { + $user->email = $user->username . "@example.com"; + } + + $user->auth = 'lti'; + $user->id = user_create_user($user); + + // Get the updated user record. + $user = $DB->get_record('user', array('id' => $user->id)); + } else { + if (\enrol_lti\helper::user_match($user, $dbuser)) { + $user = $dbuser; + } else { + // If email is empty remove it, so we don't update the user with an empty email. + if (empty($user->email)) { + unset($user->email); + } + + $user->id = $dbuser->id; + user_update_user($user); + + // Get the updated user record. + $user = $DB->get_record('user', array('id' => $user->id)); + } + } + + // Update user image. + $image = false; + if (!empty($ltirequest->info['user_image'])) { + $image = $ltirequest->info['user_image']; + } else if (!empty($ltirequest->info['custom_user_image'])) { + $image = $ltirequest->info['custom_user_image']; + } + + // Check if there is an image to process. + if ($image) { + \enrol_lti\helper::update_user_profile_image($user->id, $image); + } + + // Check if we are an instructor. + $isinstructor = $ltirequest->isInstructor(); + + if ($context->contextlevel == CONTEXT_COURSE) { + $courseid = $context->instanceid; + $urltogo = new moodle_url('/course/view.php', array('id' => $courseid)); + + // May still be set from previous session, so unset it. + unset($SESSION->forcepagelayout); + } else if ($context->contextlevel == CONTEXT_MODULE) { + $cmid = $context->instanceid; + $cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST); + $urltogo = new moodle_url('/mod/' . $cm->modname . '/view.php', array('id' => $cm->id)); + + // If we are a student in the course module context we do not want to display blocks. + if (!$isinstructor) { + // Force the page layout. + $SESSION->forcepagelayout = 'embedded'; + } else { + // May still be set from previous session, so unset it. + unset($SESSION->forcepagelayout); + } + } else { + print_error('invalidcontext'); + exit(); + } + + // Enrol the user in the course with no role. + $result = \enrol_lti\helper::enrol_user($tool, $user->id); + + // Display an error, if there is one. + if ($result !== \enrol_lti\helper::ENROLMENT_SUCCESSFUL) { + print_error($result, 'enrol_lti'); + exit(); + } + + // Give the user the role in the given context. + $roleid = $isinstructor ? $tool->roleinstructor : $tool->rolelearner; + role_assign($roleid, $user->id, $tool->contextid); + + // Login user. + $sourceid = (!empty($ltirequest->info['lis_result_sourcedid'])) ? $ltirequest->info['lis_result_sourcedid'] : ''; + $serviceurl = (!empty($ltirequest->info['lis_outcome_service_url'])) ? $ltirequest->info['lis_outcome_service_url'] : ''; + + // Check if we have recorded this user before. + if ($userlog = $DB->get_record('enrol_lti_users', array('toolid' => $tool->id, 'userid' => $user->id))) { + if ($userlog->sourceid != $sourceid) { + $userlog->sourceid = $sourceid; + } + if ($userlog->serviceurl != $serviceurl) { + $userlog->serviceurl = $serviceurl; + } + $userlog->lastaccess = time(); + $DB->update_record('enrol_lti_users', $userlog); + } else { + // Add the user details so we can use it later when syncing grades and members. + $userlog = new stdClass(); + $userlog->userid = $user->id; + $userlog->toolid = $tool->id; + $userlog->serviceurl = $serviceurl; + $userlog->sourceid = $sourceid; + $userlog->consumerkey = $ltirequest->info['oauth_consumer_key']; + $userlog->consumersecret = $tool->secret; + $userlog->lastgrade = 0; + $userlog->lastaccess = time(); + $userlog->timecreated = time(); + + if (!empty($ltirequest->info['ext_ims_lis_memberships_url'])) { + $userlog->membershipsurl = $ltirequest->info['ext_ims_lis_memberships_url']; + } else { + $userlog->membershipsurl = ''; + } + + if (!empty($ltirequest->info['ext_ims_lis_memberships_id'])) { + $userlog->membershipsid = $ltirequest->info['ext_ims_lis_memberships_id']; + } else { + $userlog->membershipsid = ''; + } + $DB->insert_record('enrol_lti_users', $userlog); + } + + // Finalise the user log in. + complete_user_login($user); + + // All done, redirect the user to where they want to go. + redirect($urltogo); +} else { + echo $ltirequest->message; +} diff --git a/enrol/lti/version.php b/enrol/lti/version.php new file mode 100644 index 00000000000..b8d6ff98df9 --- /dev/null +++ b/enrol/lti/version.php @@ -0,0 +1,29 @@ +. + +/** + * LTI enrolment plugin version information + * + * @package enrol_lti + * @copyright 2016 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2016042200; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2016041500; // Requires this Moodle version (3.1) +$plugin->component = 'enrol_lti'; // Full name of the plugin (used for diagnostics). diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index 88d76c3c9a0..6a978ebb71a 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1772,7 +1772,7 @@ class core_plugin_manager { 'enrol' => array( 'category', 'cohort', 'database', 'flatfile', - 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet', + 'guest', 'imsenterprise', 'ldap', 'lti', 'manual', 'meta', 'mnet', 'paypal', 'self' ),