Skip to content
Snippets Groups Projects
LtiLink.php 11.6 KiB
Newer Older
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) {
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
            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);
            $request->withQueryParams($launch_params),
            $this->signature_method