Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle 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 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

namespace enrol_lti\local\ltiadvantage\service;

use enrol_lti\helper;
use enrol_lti\local\ltiadvantage\entity\context;
use enrol_lti\local\ltiadvantage\entity\deployment;
use enrol_lti\local\ltiadvantage\entity\migration_claim;
use enrol_lti\local\ltiadvantage\entity\resource_link;
use enrol_lti\local\ltiadvantage\entity\user;
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
use enrol_lti\local\ltiadvantage\repository\context_repository;
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
use enrol_lti\local\ltiadvantage\repository\legacy_consumer_repository;
use enrol_lti\local\ltiadvantage\repository\resource_link_repository;
use enrol_lti\local\ltiadvantage\repository\user_repository;
use Packback\Lti1p3\LtiMessageLaunch;

/**
 * Class tool_launch_service.
 *
 * This class handles the launch of a resource by a user, using the LTI Advantage Resource Link Launch.
 *
 * See http://www.imsglobal.org/spec/lti/v1p3/#launch-from-a-resource-link
 *
 * @package enrol_lti
 * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class tool_launch_service {

    /** @var deployment_repository $deploymentrepo instance of a deployment repository. */
    private $deploymentrepo;

    /** @var application_registration_repository instance of a application_registration repository */
    private $registrationrepo;

    /** @var resource_link_repository instance of a resource_link repository */
    private $resourcelinkrepo;

    /** @var user_repository instance of a user repository*/
    private $userrepo;

    /** @var context_repository instance of a context repository  */
    private $contextrepo;

    /**
     * The tool_launch_service constructor.
     *
     * @param deployment_repository $deploymentrepo instance of a deployment_repository.
     * @param application_registration_repository $registrationrepo instance of an application_registration_repository.
     * @param resource_link_repository $resourcelinkrepo instance of a resource_link_repository.
     * @param user_repository $userrepo instance of a user_repository.
     * @param context_repository $contextrepo instance of a context_repository.
     */
    public function __construct(deployment_repository $deploymentrepo,
            application_registration_repository $registrationrepo, resource_link_repository $resourcelinkrepo,
            user_repository $userrepo, context_repository $contextrepo) {

        $this->deploymentrepo = $deploymentrepo;
        $this->registrationrepo = $registrationrepo;
        $this->resourcelinkrepo = $resourcelinkrepo;
        $this->userrepo = $userrepo;
        $this->contextrepo = $contextrepo;
    }

    /** Get the launch data from the launch.
     *
     * @param LtiMessageLaunch $launch the launch instance.
     * @return \stdClass the launch data.
     */
    private function get_launch_data(LtiMessageLaunch $launch): \stdClass {
        $launchdata = $launch->getLaunchData();
        $data = [
            'platform' => $launchdata['iss'],
            // The 'aud' property may be an array with one or more values, but can be a string if there is only one value.
            // https://www.imsglobal.org/spec/security/v1p1#id-token.
            'clientid' => is_array($launchdata['aud']) ? $launchdata['aud'][0] : $launchdata['aud'],
            'exp' => $launchdata['exp'],
            'nonce' => $launchdata['nonce'],
            'sub' => $launchdata['sub'],
            'roles' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/roles'],
            'deploymentid' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/deployment_id'],
            'context' => !empty($launchdata['https://purl.imsglobal.org/spec/lti/claim/context']) ?
                $launchdata['https://purl.imsglobal.org/spec/lti/claim/context'] : null,
            'resourcelink' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/resource_link'],
            'targetlinkuri' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/target_link_uri'],
            'custom' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/custom'] ?? null,
            'launchid' => $launch->getLaunchId(),
            'user' => [
                'givenname' => !empty($launchdata['given_name']) ? $launchdata['given_name'] : null,
                'familyname' => !empty($launchdata['family_name']) ? $launchdata['family_name'] : null,
                'name' => !empty($launchdata['name']) ? $launchdata['name'] : null,
                'email' => !empty($launchdata['email']) ? $launchdata['email'] : null,
                'picture' => !empty($launchdata['picture']) ? $launchdata['picture'] : null,
            ],
            'ags' => $launchdata['https://purl.imsglobal.org/spec/lti-ags/claim/endpoint'] ?? null,
            'nrps' => $launchdata['https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice'] ?? null,
            'lti1p1' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/lti1p1'] ?? null
        ];

        return (object) $data;
    }

    /**
     * Get a context instance from the launch data.
     *
     * @param \stdClass $launchdata launch data.
     * @param deployment $deployment the deployment to which the context belongs.
     * @return context the context instance.
     */
    private function context_from_launchdata(\stdClass $launchdata, deployment $deployment): context {
        if ($context = $this->contextrepo->find_by_contextid($launchdata->context['id'], $deployment->get_id())) {
            // The context has been mapped, just update it.
            $context->set_types($launchdata->context['type']);
        } else {
            // Map a new context.
            $context = $deployment->add_context($launchdata->context['id'], $launchdata->context['type']);
        }
        return $context;
    }

    /**
     * Get a resource_link from the launch data.
     *
     * @param \stdClass $launchdata the launch data.
     * @param \stdClass $resource the resource to which the resource link refers.
     * @param deployment $deployment the deployment to which the resource_link belongs.
     * @param context|null $context optional context in which the resource_link lives, null if not needed.
     * @return resource_link the resource_link instance.
     */
    private function resource_link_from_launchdata(\stdClass $launchdata, \stdClass $resource, deployment $deployment,
            ?context $context): resource_link {

        if ($resourcelink = $this->resourcelinkrepo->find_by_deployment($deployment, $launchdata->resourcelink['id'])) {
            // Resource link exists, so update it.
            if (isset($context)) {
                $resourcelink->set_contextid($context->get_id());
            }
            // A resource link may have been updated, via content item selection, to refer to a different resource.
            if ($resourcelink->get_resourceid() != $resource->id) {
                $resourcelink->set_resourceid($resource->id);
            }
        } else {
            // Create a new resource link.
            $resourcelink = $deployment->add_resource_link(
                $launchdata->resourcelink['id'],
                $resource->id,
                $context ? $context->get_id() : null
            );
        }
        // Add the AGS configuration for the resource link.
        // See: http://www.imsglobal.org/spec/lti-ags/v2p0#assignment-and-grade-service-claim.
        if ($launchdata->ags && (!empty($launchdata->ags['lineitems']) || !empty($launchdata->ags['lineitem']))) {
            $resourcelink->add_grade_service(
                !empty($launchdata->ags['lineitems']) ? new \moodle_url($launchdata->ags['lineitems']) : null,
                !empty($launchdata->ags['lineitem']) ? new \moodle_url($launchdata->ags['lineitem']) : null,
                $launchdata->ags['scope']
            );
        }

        // NRPS.
        if ($launchdata->nrps) {
            $resourcelink->add_names_and_roles_service(
                new \moodle_url($launchdata->nrps['context_memberships_url']),
                $launchdata->nrps['service_versions']
            );
        }
        return $resourcelink;
    }

    /**
     * Get an lti user instance from the launch data.
     *
     * @param \stdClass $user the moodle user object.
     * @param \stdClass $launchdata the launch data.
     * @param \stdClass $resource the resource to which the user belongs.
     * @param resource_link $resourcelink the resource_link from which the user originated.
     * @return user the user instance.
     */
    private function lti_user_from_launchdata(\stdClass $user, \stdClass $launchdata, \stdClass $resource,
            resource_link $resourcelink): user {

        // Find the user based on the unique-to-the-issuer 'sub' value.
        if ($ltiuser = $this->userrepo->find_single_user_by_resource($user->id, $resource->id)) {
            // User exists, so update existing based on resource data which may have changed.
            $ltiuser->set_resourcelinkid($resourcelink->get_id());
            $ltiuser->set_lang($resource->lang);
            $ltiuser->set_city($resource->city);
            $ltiuser->set_country($resource->country);
            $ltiuser->set_institution($resource->institution);
            $ltiuser->set_timezone($resource->timezone);
            $ltiuser->set_maildisplay($resource->maildisplay);
        } else {
            // Create the lti user.
            $ltiuser = $resourcelink->add_user(
                $user->id,
                $launchdata->sub,
                $resource->lang,
                $resource->city ?? '',
                $resource->country ?? '',
                $resource->institution ?? '',
                $resource->timezone ?? '',
                $resource->maildisplay ?? null,
            );
        }
        $ltiuser->set_lastaccess(time());
        return $ltiuser;
    }

    /**
     * Get the migration claim from the launch data, or null if not found.
     *
     * @param \stdClass $launchdata the launch data.
     * @return migration_claim|null the claim instance if present in the launch data, else null.
     */
    private function migration_claim_from_launchdata(\stdClass $launchdata): ?migration_claim {
        if (!isset($launchdata->lti1p1)) {
            return null;
        }

        // Despite the spec requiring the oauth_consumer_key field be present in the migration claim:
        // (see https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key),
        // Platforms may omit this field making migration impossible.
        // E.g. for Canvas launches taking place after an assignment_selection placement.
        if (empty($launchdata->lti1p1['oauth_consumer_key'])) {
            return null;
        }

        return new migration_claim($launchdata->lti1p1, $launchdata->deploymentid,
            $launchdata->platform, $launchdata->clientid, $launchdata->exp, $launchdata->nonce,
            new legacy_consumer_repository());
    }

    /**
     * Check whether the launch user has an admin role.
     *
     * @param \stdClass $launchdata the launch data.
     * @return bool true if the user is admin, false otherwise.
     */
    private function user_is_admin(\stdClass $launchdata): bool {
        // See: http://www.imsglobal.org/spec/lti/v1p3/#role-vocabularies.
        if ($launchdata->roles) {
            $adminroles = [
                'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator',
                'http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator'
            ];

            foreach ($adminroles as $validrole) {
                if (in_array($validrole, $launchdata->roles)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Check whether the launch user is an instructor.
     *
     * @param \stdClass $launchdata the launch data.
     * @param bool $includelegacy whether to also consider legacy simple names as valid roles.
     * @return bool true if the user is an instructor, false otherwise.
     */
    private function user_is_staff(\stdClass $launchdata, bool $includelegacy = false): bool {
        // See: http://www.imsglobal.org/spec/lti/v1p3/#role-vocabularies.
        // This method also provides support for (legacy, deprecated) simple names for context roles.
        // I.e. 'ContentDeveloper' may be supported.
        if ($launchdata->roles) {
            $staffroles = [
                'http://purl.imsglobal.org/vocab/lis/v2/membership#ContentDeveloper',
                'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor',
                'http://purl.imsglobal.org/vocab/lis/v2/membership/Instructor#TeachingAssistant'
            ];

            if ($includelegacy) {
                $staffroles[] = 'ContentDeveloper';
                $staffroles[] = 'Instructor';
                $staffroles[] = 'Instructor#TeachingAssistant';
            }

            foreach ($staffroles as $validrole) {
                if (in_array($validrole, $launchdata->roles)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Handles the use case "A user launches the tool so they can view an external resource".
     *
     * @param \stdClass $user the Moodle user record, obtained via the auth_lti authentication process.
     * @param LtiMessageLaunch $launch the launch data.
     * @return array array containing [int $userid, \stdClass $resource]
     * @throws \moodle_exception if launch problems are encountered.
     */
    public function user_launches_tool(\stdClass $user, LtiMessageLaunch $launch): array {

        $launchdata = $this->get_launch_data($launch);

        if (!$registration = $this->registrationrepo->find_by_platform($launchdata->platform, $launchdata->clientid)) {
            throw new \moodle_exception('ltiadvlauncherror:invalidregistration', 'enrol_lti', '',
                [$launchdata->platform, $launchdata->clientid]);
        }

        if (!$deployment = $this->deploymentrepo->find_by_registration($registration->get_id(),
            $launchdata->deploymentid)) {
            throw new \moodle_exception('ltiadvlauncherror:invaliddeployment', 'enrol_lti', '',
                [$launchdata->deploymentid]);
        }

        $resourceuuid = $launchdata->custom['id'] ?? null;
        if (empty($resourceuuid)) {
            throw new \moodle_exception('ltiadvlauncherror:missingid', 'enrol_lti');
        }

        $resource = array_values(helper::get_lti_tools(['uuid' => $resourceuuid]));
        $resource = $resource[0] ?? null;
        if (empty($resource) || $resource->status != ENROL_INSTANCE_ENABLED) {
            throw new \moodle_exception('ltiadvlauncherror:invalidid', 'enrol_lti', '', $resourceuuid);
        }

        // Update the deployment with the legacy consumer_key information, allowing migration of users to take place in future
        // names and roles syncs.
        if ($migrationclaim = $this->migration_claim_from_launchdata($launchdata)) {
            $deployment->set_legacy_consumer_key($migrationclaim->get_consumer_key());
            $this->deploymentrepo->save($deployment);
        }

        // Save the context, if that claim is present.
        $context = null;
        if ($launchdata->context) {
            $context = $this->context_from_launchdata($launchdata, $deployment);
            $context = $this->contextrepo->save($context);
        }

        // Save the resource link for the tool deployment.
        $resourcelink = $this->resource_link_from_launchdata($launchdata, $resource, $deployment, $context);
        $resourcelink = $this->resourcelinkrepo->save($resourcelink);

        // Save the user launching the resource link.
        $ltiuser = $this->lti_user_from_launchdata($user, $launchdata, $resource, $resourcelink);
        $ltiuser = $this->userrepo->save($ltiuser);

        // Set the frame embedding mode, which controls the display of blocks and nav when launching.
        global $SESSION;
        $context = \context::instance_by_id($resource->contextid);
        $isforceembed = $launchdata->custom['force_embed'] ?? false;
        $isinstructor = $this->user_is_staff($launchdata, true) || $this->user_is_admin($launchdata);
        $isforceembed = $isforceembed || ($context->contextlevel == CONTEXT_MODULE && !$isinstructor);
        if ($isforceembed) {
            $SESSION->forcepagelayout = 'embedded';
        } else {
            unset($SESSION->forcepagelayout);
        }

        // Enrol the user in the course with no role.
        $result = helper::enrol_user($resource, $ltiuser->get_localid());
        if ($result !== helper::ENROLMENT_SUCCESSFUL) {
            throw new \moodle_exception($result, 'enrol_lti');
        }

        // Give the user the role in the given context.
        $roleid = $isinstructor ? $resource->roleinstructor : $resource->rolelearner;
        role_assign($roleid, $ltiuser->get_localid(), $resource->contextid);

        return [$ltiuser->get_localid(), $resource];
    }

}