<?php use Psr\Http\Message\ServerRequestFactoryInterface; /** * LtiLink.php - LTI 1.x link representation for Stud.IP * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * @author Elmar Ludwig * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 */ /** * The LtiLink class represents an LTI 1.x link inside the LMS. It stores the * launch URL and the corresponding credentials as well as a list of launch * parameters and custom parameter substitution variables. * * Use LtiLink::getLaunchSignature() to fetch the OAuth signature to use for * the launch request (or some other request). */ class LtiLink { // launch URL and credentials protected $launch_url; protected $consumer_key; protected $consumer_secret; protected $signature_method; // launch parameters and variables protected $parameters = []; protected $variables = []; /** * Iniialize a new LtiLink instance with the given URL and credentials. * * @param string $launch_url launch URL of external LTI tool * @param string $consumer_key consumer key of the LTI link * @param string $consumer_secret consumer secret of the LTI link * @param string $signature_method signature method to use (optional) */ public function __construct($launch_url, $consumer_key, $consumer_secret, $signature_method = 'sha1') { $this->launch_url = $launch_url; $this->consumer_key = $consumer_key; $this->consumer_secret = $consumer_secret; $this->signature_method = $signature_method; // Basic LTI uses OAuth to sign requests // OAuth Core 1.0 spec: http://oauth.net/core/1.0/ $this->addLaunchParameters([ 'lti_version' => 'LTI-1p0', 'lti_message_type' => 'basic-lti-launch-request', 'oauth_consumer_key' => $this->consumer_key, 'oauth_callback' => 'about:blank', 'oauth_version' => '1.0', 'oauth_nonce' => uniqid('lti', true), 'oauth_timestamp' => time(), 'oauth_signature_method' => 'HMAC-' . strtoupper($this->signature_method), 'tool_consumer_info_product_family_code' => 'studip', 'tool_consumer_info_version' => $GLOBALS['SOFTWARE_VERSION'], 'tool_consumer_instance_guid' => Config::get()->STUDIP_INSTALLATION_ID, 'tool_consumer_instance_name' => Config::get()->UNI_NAME_CLEAN, 'tool_consumer_instance_description' => $GLOBALS['UNI_INFO'], 'tool_consumer_instance_url' => $GLOBALS['ABSOLUTE_URI_STUDIP'], 'tool_consumer_instance_contact_email' => $GLOBALS['UNI_CONTACT'] ]); } /** * Set the LMS resource associated with this LTI link. This is required * for an LTI launch request. * * @param string $resource_id id of associated resource * @param string $resource_title title of associated resource * @param string $resource_description description of associated resource */ public function setResource($resource_id, $resource_title, $resource_description = null) { $this->addVariables([ 'ResourceLink.id' => $resource_id, 'ResourceLink.title' => $resource_title, 'ResourceLink.description' => $resource_description, ]); $this->addLaunchParameters([ 'resource_link_id' => $this->variables['ResourceLink.id'], 'resource_link_title' => $this->variables['ResourceLink.title'], 'resource_link_description' => $this->variables['ResourceLink.description'], ]); } /** * Set the Stud.IP course associated with this LTI link. The course data * is used to set up the context and course parameters and variables. * * @param string $course_id id of associated course */ public function setCourse($course_id) { $course = Course::find($course_id); $this->addVariable('Context.id', $course_id); $this->addLaunchParameter('context_id', $course_id); if ($course) { $this->addVariables([ 'Context.type' => 'CourseSection', 'Context.label' => $course->veranstaltungsnummer, 'Context.title' => $course->name, 'CourseSection.sourcedId' => $course->id, 'CourseSection.label' => $course->veranstaltungsnummer, 'CourseSection.title' => $course->name, 'CourseSection.shortDescription' => $course->untertitel, 'CourseSection.longDescription' => $course->beschreibung, 'CourseSection.courseNumber' => $course->veranstaltungsnummer, 'CourseSection.credits' => $course->ects, 'CourseSection.maxNumberofStudents' => $course->admission_turnout, 'CourseSection.numberofStudents' => $course->getNumParticipants(), 'CourseSection.dept' => $course->home_institut->name ?? null, ]); $this->addLaunchParameters([ 'context_type' => $this->variables['Context.type'], 'context_label' => $this->variables['Context.label'], 'context_title' => $this->variables['Context.title'], 'lis_course_section_sourcedid' => $this->variables['CourseSection.sourcedId'], ]); } } /** * Set the Stud.IP user associated with this LTI launch. The user data * is used to set up the user and LIS person parameters and variables. * If send_lis_person is true, the user's name and e-mail is included. * * @param string $user_id id of associated course * @param string $roles roles of this user (defaults to 'Learner') * @param bool $send_lis_person include additional user information */ public function setUser($user_id, $roles = 'Learner', $send_lis_person = false) { $user = User::find($user_id); $avatar = Avatar::getAvatar($user_id); $this->addVariable('User.id', $user_id); $this->addLaunchParameter('user_id', $user_id); $this->addLaunchParameter('roles', $roles); if ($user && $send_lis_person) { $this->addVariables([ 'User.image' => $avatar->getURL(Avatar::NORMAL), 'User.username' => $user->username, 'Person.sourcedId' => $user->username, 'Person.name.full' => $user->getFullName(), 'Person.name.family' => $user->nachname, 'Person.name.given' => $user->vorname, 'Person.name.prefix' => $user->title_front, 'Person.name.suffix' => $user->title_rear, 'Person.email.primary' => $user->email, 'Person.webaddress' => $user->home, ]); $this->addLaunchParameters([ 'lis_person_name_full' => $this->variables['Person.name.full'], 'lis_person_name_family' => $this->variables['Person.name.family'], 'lis_person_name_given' => $this->variables['Person.name.given'], 'lis_person_contact_email_primary' => $this->variables['Person.email.primary'], 'lis_person_sourcedid' => $this->variables['Person.sourcedId'], ]); } } /** * Add an additional launch parameter to this LTI launch request. * * @param string $name parameter name * @param string $value value (use NULL to unset) */ public function addLaunchParameter($name, $value) { if (isset($value)) { $this->parameters[$name] = (string) $value; } else { unset($this->parameters[$name]); } } /** * Add a list of additional launch parameters to this LTI launch request. * * @param array $params list of launch parameters */ public function addLaunchParameters($params) { foreach ($params as $key => $value) { $this->addLaunchParameter($key, $value); } } /** * Add a custom launch parameter to this LTI launch request. All custom * parameter names are prefixed with 'custom_' and variable substitution * is applied. * * @param string $name parameter name * @param string $value value (may contain variables) */ public function addCustomParameter($name, $value) { $name = strtolower(preg_replace('/\W/', '_', $name)); $value = preg_replace_callback('/\$([\w.]*\w)/', function($matches) { return $this->variables[$matches[1]] ?? $matches[0]; }, $value); $this->addLaunchParameter('custom_' . $name, $value); } /** * Add a list of custom launch parameters to this LTI launch request. * * @param array $params list of custom parameters */ public function addCustomParameters($params) { foreach ($params as $key => $value) { $this->addCustomParameter($key, $value); } } /** * Add a substitution variable to this LTI launch request. * * @param string $name variable name * @param string $value value (use NULL to unset) */ public function addVariable($name, $value) { if (isset($value)) { $this->variables[$name] = $value; } else { unset($this->variables[$name]); } } /** * Add a list of substitution variables to this LTI launch request. * * @param array $variables list of substitution variables */ public function addVariables($variables) { foreach ($variables as $key => $value) { $this->addVariable($key, $value); } } /** * Get the substitution variables defined for this LTI link. * * @return array list of substitution variables */ public function getVariables() { return $this->variables; } /** * Get the launch URL for this LTI link. * * @return string launch URL of external LTI tool */ public function getLaunchURL() { return $this->launch_url; } /** * Get the launch parameters for the LTI basic launch request. * * @return array launch parameters (UTF-8 encoded) */ public function getBasicLaunchData() { return $this->parameters; } /** * Sign a launch request including the given launch parameters. * * @param array $launch_params array of launch parameters * * @return string launch signature */ public function getLaunchSignature($launch_params) { $launch_url = $this->launch_url; if (strpos($launch_url, '#') !== false) { $launch_url = explode('#', $launch_url)[0]; } if (strpos($launch_url, '?') !== false) { list($launch_url, $query) = explode('?', $launch_url); } if (isset($query)) { parse_str($query, $query_params); $launch_params += $query_params; } // posted form data will always use CR LF $launch_params = preg_replace("/\r?\n/", "\r\n", $launch_params); $requestFactory = app(ServerRequestFactoryInterface::class); $request = $requestFactory->createServerRequest('POST', $launch_url); return Studip\OAuth1::signRequest( $request->withQueryParams($launch_params), $this->consumer_secret, '', $this->signature_method ); } }