MDL-76926 libraries: upgrade lib/lti1p3 to patched v5.2.6

This commit is contained in:
Jake Dallimore 2023-03-02 15:13:39 +08:00
parent 5e1df25566
commit efae8d36d5
20 changed files with 146 additions and 158 deletions

View File

@ -4,13 +4,11 @@ A library used for building IMS-certified LTI 1.3 tool providers in PHP.
This library is a fork of the [packbackbooks/lti-1-3-php-library](https://github.com/packbackbooks/lti-1-3-php-library), patched specifically for use in [Moodle](https://github.com/moodle/moodle).
It is currently based on version [5.2.1 of the packbackbooks/lti-1-3-php-library](https://github.com/packbackbooks/lti-1-3-php-library/releases/tag/v5.2.1) library.
It is currently based on version [5.2.6 of the packbackbooks/lti-1-3-php-library](https://github.com/packbackbooks/lti-1-3-php-library/releases/tag/v5.2.6) library.
The following changes are included so that the library may be used with Moodle:
* Replace the phpseclib dependency with openssl equivalent call in public key generation code.
* Replace the Guzzle dependency with generic HTTP client interfaces for client, response, exception.
* Small fix to http_build_query() calls, which now explicitly include the '&' arg separator param, for compatibility with applications that override PHP's arg_separator.output value via an ini_set() call, like Moodle does.
Please see the original [README](https://github.com/packbackbooks/lti-1-3-php-library/blob/master/README.md) for more information about the upstream library.

View File

@ -4,9 +4,6 @@ namespace Packback\Lti1p3\Helpers;
class Helpers
{
/**
* @param $value
*/
public static function checkIfNullValue($value): bool
{
return !is_null($value);

View File

@ -1,8 +0,0 @@
<?php
namespace Packback\Lti1p3\Interfaces;
Interface IHttpClient
{
public function request(string $method, string $url, array $options) : IHttpResponse;
}

View File

@ -1,8 +0,0 @@
<?php
namespace Packback\Lti1p3\Interfaces;
interface IHttpException extends \Throwable
{
public function getResponse(): IHttpResponse;
}

View File

@ -1,10 +0,0 @@
<?php
namespace Packback\Lti1p3\Interfaces;
interface IHttpResponse
{
public function getBody();
public function getHeaders(): array;
public function getStatusCode(): int;
}

View File

@ -2,6 +2,7 @@
namespace Packback\Lti1p3\Interfaces;
/** @internal */
interface ILtiRegistration
{
public function getIssuer();

View File

@ -2,13 +2,15 @@
namespace Packback\Lti1p3\Interfaces;
use GuzzleHttp\Psr7\Response;
interface ILtiServiceConnector
{
public function getAccessToken(ILtiRegistration $registration, array $scopes);
public function makeRequest(IServiceRequest $request);
public function getResponseBody(IHttpResponse $response): ?array;
public function getResponseBody(Response $request): ?array;
public function makeServiceRequest(
ILtiRegistration $registration,

View File

@ -2,9 +2,12 @@
namespace Packback\Lti1p3\Interfaces;
/** @internal */
interface IMessageValidator
{
public function validate(array $jwtBody);
public static function getMessageType(): string;
public function canValidate(array $jwtBody);
public static function canValidate(array $jwtBody): bool;
public static function validate(array $jwtBody): void;
}

View File

@ -2,6 +2,7 @@
namespace Packback\Lti1p3\Interfaces;
/** @internal */
interface IServiceRequest
{
public function getMethod(): string;

View File

@ -15,8 +15,8 @@ abstract class LtiAbstractService
public function __construct(
ILtiServiceConnector $serviceConnector,
ILtiRegistration $registration,
array $serviceData)
{
array $serviceData
) {
$this->serviceConnector = $serviceConnector;
$this->registration = $registration;
$this->serviceData = $serviceData;

View File

@ -91,4 +91,9 @@ class LtiConstants
public const COURSE_OFFERING = 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseOffering';
public const COURSE_SECTION = 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseSection';
public const COURSE_GROUP = 'http://purl.imsglobal.org/vocab/lis/v2/course#Group';
// Message Types
public const MESSAGE_TYPE_DEEPLINK = 'LtiDeepLinkingRequest';
public const MESSAGE_TYPE_RESOURCE = 'LtiResourceLinkRequest';
public const MESSAGE_TYPE_SUBMISSIONREVIEW = 'LtiSubmissionReviewRequest';
}

View File

@ -29,7 +29,9 @@ class LtiDeepLink
LtiConstants::DEPLOYMENT_ID => $this->deployment_id,
LtiConstants::MESSAGE_TYPE => 'LtiDeepLinkingResponse',
LtiConstants::VERSION => LtiConstants::V1_3,
LtiConstants::DL_CONTENT_ITEMS => array_map(function ($resource) { return $resource->toArray(); }, $resources),
LtiConstants::DL_CONTENT_ITEMS => array_map(function ($resource) {
return $resource->toArray();
}, $resources),
];
// https://www.imsglobal.org/spec/lti-dl/v2p0/#deep-linking-request-message

View File

@ -12,6 +12,7 @@ class LtiGrade
private $timestamp;
private $user_id;
private $submission_review;
private $canvas_extension;
public function __construct(array $grade = null)
{

View File

@ -2,13 +2,15 @@
namespace Packback\Lti1p3;
use Exception;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\TransferException;
use Packback\Lti1p3\Interfaces\ICache;
use Packback\Lti1p3\Interfaces\ICookie;
use Packback\Lti1p3\Interfaces\IDatabase;
use Packback\Lti1p3\Interfaces\IHttpException;
use Packback\Lti1p3\Interfaces\ILtiServiceConnector;
use Packback\Lti1p3\MessageValidators\DeepLinkMessageValidator;
use Packback\Lti1p3\MessageValidators\ResourceMessageValidator;
@ -41,7 +43,6 @@ class LtiMessageLaunch
public const ERR_MISSING_DEPLOYEMENT_ID = 'No deployment ID was specified';
public const ERR_NO_DEPLOYMENT = 'Unable to find deployment.';
public const ERR_INVALID_MESSAGE_TYPE = 'Invalid message type';
public const ERR_VALIDATOR_CONFLICT = 'Validator conflict.';
public const ERR_UNRECOGNIZED_MESSAGE_TYPE = 'Unrecognized message type.';
public const ERR_INVALID_MESSAGE = 'Message validation failed.';
public const ERR_INVALID_ALG = 'Invalid alg was specified in the JWT header.';
@ -69,10 +70,10 @@ class LtiMessageLaunch
/**
* Constructor.
*
* @param IDatabase $database instance of the database interface used for looking up registrations and deployments
* @param ICache $cache instance of the Cache interface used to loading and storing launches
* @param ICookie $cookie instance of the Cookie interface used to set and read cookies
* @param ILtiServiceConnector $serviceConnector instance of the LtiServiceConnector used to by LTI services to make API requests
* @param IDatabase $database Instance of the database interface used for looking up registrations and deployments
* @param ICache $cache Instance of the Cache interface used to loading and storing launches
* @param ICookie $cookie Instance of the Cookie interface used to set and read cookies
* @param ILtiServiceConnector $serviceConnector Instance of the LtiServiceConnector used to by LTI services to make API requests
*/
public function __construct(
IDatabase $database,
@ -104,19 +105,20 @@ class LtiMessageLaunch
/**
* Load an LtiMessageLaunch from a Cache using a launch id.
*
* @param string $launch_id the launch id of the LtiMessageLaunch object that is being pulled from the cache
* @param IDatabase $database instance of the database interface used for looking up registrations and deployments
* @param string $launch_id The launch id of the LtiMessageLaunch object that is being pulled from the cache
* @param IDatabase $database Instance of the database interface used for looking up registrations and deployments
* @param ICache $cache Instance of the Cache interface used to loading and storing launches. If non is provided launch data will be store in $_SESSION.
*
* @throws LtiException will throw an LtiException if validation fails or launch cannot be found
* @throws LtiException Will throw an LtiException if validation fails or launch cannot be found
*
* @return LtiMessageLaunch a populated and validated LtiMessageLaunch
* @return LtiMessageLaunch A populated and validated LtiMessageLaunch
*/
public static function fromCache($launch_id,
public static function fromCache(
$launch_id,
IDatabase $database,
ICache $cache = null,
ILtiServiceConnector $serviceConnector = null)
{
ILtiServiceConnector $serviceConnector = null
) {
$new = new LtiMessageLaunch($database, $cache, null, $serviceConnector);
$new->launch_id = $launch_id;
$new->jwt = ['body' => $new->cache->getLaunchData($launch_id)];
@ -129,9 +131,9 @@ class LtiMessageLaunch
*
* @param array|string $request An array of post request parameters. If not set will default to $_POST.
*
* @throws LtiException will throw an LtiException if validation fails
* @throws LtiException Will throw an LtiException if validation fails
*
* @return LtiMessageLaunch will return $this if validation is successful
* @return LtiMessageLaunch Will return $this if validation is successful
*/
public function validate(array $request = null)
{
@ -153,7 +155,7 @@ class LtiMessageLaunch
/**
* Returns whether or not the current launch can use the names and roles service.
*
* @return bool returns a boolean indicating the availability of names and roles
* @return bool Returns a boolean indicating the availability of names and roles
*/
public function hasNrps()
{
@ -163,20 +165,21 @@ class LtiMessageLaunch
/**
* Fetches an instance of the names and roles service for the current launch.
*
* @return LtiNamesRolesProvisioningService an instance of the names and roles service that can be used to make calls within the scope of the current launch
* @return LtiNamesRolesProvisioningService An instance of the names and roles service that can be used to make calls within the scope of the current launch
*/
public function getNrps()
{
return new LtiNamesRolesProvisioningService(
$this->serviceConnector,
$this->registration,
$this->jwt['body'][LtiConstants::NRPS_CLAIM_SERVICE]);
$this->jwt['body'][LtiConstants::NRPS_CLAIM_SERVICE]
);
}
/**
* Returns whether or not the current launch can use the groups service.
*
* @return bool returns a boolean indicating the availability of groups
* @return bool Returns a boolean indicating the availability of groups
*/
public function hasGs()
{
@ -186,20 +189,21 @@ class LtiMessageLaunch
/**
* Fetches an instance of the groups service for the current launch.
*
* @return LtiCourseGroupsService an instance of the groups service that can be used to make calls within the scope of the current launch
* @return LtiCourseGroupsService An instance of the groups service that can be used to make calls within the scope of the current launch
*/
public function getGs()
{
return new LtiCourseGroupsService(
$this->serviceConnector,
$this->registration,
$this->jwt['body'][LtiConstants::GS_CLAIM_SERVICE]);
$this->jwt['body'][LtiConstants::GS_CLAIM_SERVICE]
);
}
/**
* Returns whether or not the current launch can use the assignments and grades service.
*
* @return bool returns a boolean indicating the availability of assignments and grades
* @return bool Returns a boolean indicating the availability of assignments and grades
*/
public function hasAgs()
{
@ -209,20 +213,21 @@ class LtiMessageLaunch
/**
* Fetches an instance of the assignments and grades service for the current launch.
*
* @return LtiAssignmentsGradesService an instance of the assignments an grades service that can be used to make calls within the scope of the current launch
* @return LtiAssignmentsGradesService An instance of the assignments an grades service that can be used to make calls within the scope of the current launch
*/
public function getAgs()
{
return new LtiAssignmentsGradesService(
$this->serviceConnector,
$this->registration,
$this->jwt['body'][LtiConstants::AGS_CLAIM_ENDPOINT]);
$this->jwt['body'][LtiConstants::AGS_CLAIM_ENDPOINT]
);
}
/**
* Returns whether or not the current launch is a deep linking launch.
*
* @return bool returns true if the current launch is a deep linking launch
* @return bool Returns true if the current launch is a deep linking launch
*/
public function isDeepLinkLaunch()
{
@ -232,20 +237,21 @@ class LtiMessageLaunch
/**
* Fetches a deep link that can be used to construct a deep linking response.
*
* @return LtiDeepLink an instance of a deep link to construct a deep linking response for the current launch
* @return LtiDeepLink An instance of a deep link to construct a deep linking response for the current launch
*/
public function getDeepLink()
{
return new LtiDeepLink(
$this->registration,
$this->jwt['body'][LtiConstants::DEPLOYMENT_ID],
$this->jwt['body'][LtiConstants::DL_DEEP_LINK_SETTINGS]);
$this->jwt['body'][LtiConstants::DL_DEEP_LINK_SETTINGS]
);
}
/**
* Returns whether or not the current launch is a submission review launch.
*
* @return bool returns true if the current launch is a submission review launch
* @return bool Returns true if the current launch is a submission review launch
*/
public function isSubmissionReviewLaunch()
{
@ -255,7 +261,7 @@ class LtiMessageLaunch
/**
* Returns whether or not the current launch is a resource launch.
*
* @return bool returns true if the current launch is a resource launch
* @return bool Returns true if the current launch is a resource launch
*/
public function isResourceLaunch()
{
@ -265,7 +271,7 @@ class LtiMessageLaunch
/**
* Fetches the decoded body of the JWT used in the current launch.
*
* @return array|object returns the decoded json body of the launch as an array
* @return array|object Returns the decoded json body of the launch as an array
*/
public function getLaunchData()
{
@ -275,7 +281,7 @@ class LtiMessageLaunch
/**
* Get the unique launch id for the current launch.
*
* @return string a unique identifier used to re-reference the current launch in subsequent requests
* @return string A unique identifier used to re-reference the current launch in subsequent requests
*/
public function getLaunchId()
{
@ -306,7 +312,7 @@ class LtiMessageLaunch
// Download key set
try {
$response = $this->serviceConnector->makeRequest($request);
} catch (IHttpException $e) {
} catch (TransferException $e) {
throw new LtiException(static::ERR_NO_PUBLIC_KEY);
}
$publicKeySet = $this->serviceConnector->getResponseBody($response);
@ -325,7 +331,7 @@ class LtiMessageLaunch
$keySet = JWK::parseKeySet([
'keys' => [$key],
]);
} catch (\Exception $e) {
} catch (Exception $e) {
// Do nothing
}
@ -484,36 +490,31 @@ class LtiMessageLaunch
throw new LtiException(static::ERR_INVALID_MESSAGE_TYPE);
}
/**
* @todo Fix this nonsense
*/
$validator = $this->getMessageValidator($this->jwt['body']);
// Create instances of all validators
$validators = [
new DeepLinkMessageValidator(),
new ResourceMessageValidator(),
new SubmissionReviewMessageValidator(),
];
$message_validator = false;
foreach ($validators as $validator) {
if ($validator->canValidate($this->jwt['body'])) {
if ($message_validator !== false) {
// Can't have more than one validator apply at a time.
throw new LtiException(static::ERR_VALIDATOR_CONFLICT);
}
$message_validator = $validator;
}
}
if ($message_validator === false) {
if (!isset($validator)) {
throw new LtiException(static::ERR_UNRECOGNIZED_MESSAGE_TYPE);
}
if (!$message_validator->validate($this->jwt['body'])) {
throw new LtiException(static::ERR_INVALID_MESSAGE);
}
$validator::validate($this->jwt['body']);
return $this;
}
private function getMessageValidator(array $jwtBody): ?string
{
$availableValidators = [
DeepLinkMessageValidator::class,
ResourceMessageValidator::class,
SubmissionReviewMessageValidator::class,
];
// Filter out validators that cannot validate the message
$applicableValidators = array_filter($availableValidators, function ($validator) use ($jwtBody) {
return $validator::canValidate($jwtBody);
});
// There should be 0-1 validators. This will either return the validator, or null if none apply.
return array_shift($applicableValidators);
}
}

View File

@ -21,9 +21,9 @@ class LtiOidcLogin
/**
* Constructor.
*
* @param IDatabase $database instance of the database interface used for looking up registrations and deployments
* @param ICache $cache Instance of the Cache interface used to loading and storing launches. If non is provided launch data will be store in $_SESSION.
* @param ICookie $cookie Instance of the Cookie interface used to set and read cookies. Will default to using $_COOKIE and setcookie.
* @param IDatabase $database Instance of the Database interface used for looking up registrations and deployments
* @param ICache $cache instance of the Cache interface used to loading and storing launches
* @param ICookie $cookie instance of the Cookie interface used to set and read cookies
*/
public function __construct(IDatabase $database, ICache $cache = null, ICookie $cookie = null)
{

View File

@ -2,11 +2,12 @@
namespace Packback\Lti1p3;
use Exception;
use Firebase\JWT\JWT;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Psr7\Response;
use Packback\Lti1p3\Interfaces\ICache;
use Packback\Lti1p3\Interfaces\IHttpClient;
use Packback\Lti1p3\Interfaces\IHttpException;
use Packback\Lti1p3\Interfaces\IHttpResponse;
use Packback\Lti1p3\Interfaces\ILtiRegistration;
use Packback\Lti1p3\Interfaces\ILtiServiceConnector;
use Packback\Lti1p3\Interfaces\IServiceRequest;
@ -21,7 +22,7 @@ class LtiServiceConnector implements ILtiServiceConnector
public function __construct(
ICache $cache,
IHttpClient $client
Client $client
) {
$this->cache = $cache;
$this->client = $client;
@ -100,7 +101,7 @@ class LtiServiceConnector implements ILtiServiceConnector
return $response;
}
public function getResponseHeaders(IHttpResponse $response): ?array
public function getResponseHeaders(Response $response): ?array
{
$responseHeaders = $response->getHeaders();
array_walk($responseHeaders, function (&$value) {
@ -110,7 +111,7 @@ class LtiServiceConnector implements ILtiServiceConnector
return $responseHeaders;
}
public function getResponseBody(IHttpResponse $response): ?array
public function getResponseBody(Response $response): ?array
{
$responseBody = (string) $response->getBody();
@ -127,7 +128,7 @@ class LtiServiceConnector implements ILtiServiceConnector
try {
$response = $this->makeRequest($request);
} catch (IHttpException $e) {
} catch (ClientException $e) {
$status = $e->getResponse()->getStatusCode();
// If the error was due to invalid authentication and the request
@ -156,7 +157,7 @@ class LtiServiceConnector implements ILtiServiceConnector
string $key = null
): array {
if ($request->getMethod() !== ServiceRequest::METHOD_GET) {
throw new \Exception('An invalid method was specified by an LTI service requesting all items.');
throw new Exception('An invalid method was specified by an LTI service requesting all items.');
}
$results = [];

View File

@ -0,0 +1,35 @@
<?php
namespace Packback\Lti1p3\MessageValidators;
use Packback\Lti1p3\Interfaces\IMessageValidator;
use Packback\Lti1p3\LtiConstants;
use Packback\Lti1p3\LtiException;
abstract class AbstractMessageValidator implements IMessageValidator
{
abstract public static function getMessageType(): string;
public static function canValidate(array $jwtBody): bool
{
return $jwtBody[LtiConstants::MESSAGE_TYPE] === static::getMessageType();
}
abstract public static function validate(array $jwtBody): void;
public static function validateGenericMessage(array $jwtBody): void
{
if (empty($jwtBody['sub'])) {
throw new LtiException('Must have a user (sub)');
}
if (!isset($jwtBody[LtiConstants::VERSION])) {
throw new LtiException('Missing LTI Version');
}
if ($jwtBody[LtiConstants::VERSION] !== LtiConstants::V1_3) {
throw new LtiException('Incorrect version, expected 1.3.0');
}
if (!isset($jwtBody[LtiConstants::ROLES])) {
throw new LtiException('Missing Roles Claim');
}
}
}

View File

@ -2,28 +2,20 @@
namespace Packback\Lti1p3\MessageValidators;
use Packback\Lti1p3\Interfaces\IMessageValidator;
use Packback\Lti1p3\LtiConstants;
use Packback\Lti1p3\LtiException;
class DeepLinkMessageValidator implements IMessageValidator
class DeepLinkMessageValidator extends AbstractMessageValidator
{
public function canValidate(array $jwtBody)
public static function getMessageType(): string
{
return $jwtBody[LtiConstants::MESSAGE_TYPE] === 'LtiDeepLinkingRequest';
return LtiConstants::MESSAGE_TYPE_DEEPLINK;
}
public function validate(array $jwtBody)
public static function validate(array $jwtBody): void
{
if (empty($jwtBody['sub'])) {
throw new LtiException('Must have a user (sub)');
}
if ($jwtBody[LtiConstants::VERSION] !== LtiConstants::V1_3) {
throw new LtiException('Incorrect version, expected 1.3.0');
}
if (!isset($jwtBody[LtiConstants::ROLES])) {
throw new LtiException('Missing Roles Claim');
}
static::validateGenericMessage($jwtBody);
if (empty($jwtBody[LtiConstants::DL_DEEP_LINK_SETTINGS])) {
throw new LtiException('Missing Deep Linking Settings');
}
@ -37,7 +29,5 @@ class DeepLinkMessageValidator implements IMessageValidator
if (empty($deep_link_settings['accept_presentation_document_targets'])) {
throw new LtiException('Must support a presentation type');
}
return true;
}
}

View File

@ -2,35 +2,22 @@
namespace Packback\Lti1p3\MessageValidators;
use Packback\Lti1p3\Interfaces\IMessageValidator;
use Packback\Lti1p3\LtiConstants;
use Packback\Lti1p3\LtiException;
class ResourceMessageValidator implements IMessageValidator
class ResourceMessageValidator extends AbstractMessageValidator
{
public function canValidate(array $jwtBody)
public static function getMessageType(): string
{
return $jwtBody[LtiConstants::MESSAGE_TYPE] === 'LtiResourceLinkRequest';
return LtiConstants::MESSAGE_TYPE_RESOURCE;
}
public function validate(array $jwtBody)
public static function validate(array $jwtBody): void
{
if (empty($jwtBody['sub'])) {
throw new LtiException('Must have a user (sub)');
}
if (!isset($jwtBody[LtiConstants::VERSION])) {
throw new LtiException('Missing LTI Version');
}
if ($jwtBody[LtiConstants::VERSION] !== LtiConstants::V1_3) {
throw new LtiException('Incorrect version, expected 1.3.0');
}
if (!isset($jwtBody[LtiConstants::ROLES])) {
throw new LtiException('Missing Roles Claim');
}
static::validateGenericMessage($jwtBody);
if (empty($jwtBody[LtiConstants::RESOURCE_LINK]['id'])) {
throw new LtiException('Missing Resource Link Id');
}
return true;
}
}

View File

@ -2,35 +2,25 @@
namespace Packback\Lti1p3\MessageValidators;
use Packback\Lti1p3\Interfaces\IMessageValidator;
use Packback\Lti1p3\LtiConstants;
use Packback\Lti1p3\LtiException;
class SubmissionReviewMessageValidator implements IMessageValidator
class SubmissionReviewMessageValidator extends AbstractMessageValidator
{
public function canValidate(array $jwtBody)
public static function getMessageType(): string
{
return $jwtBody[LtiConstants::MESSAGE_TYPE] === 'LtiSubmissionReviewRequest';
return LtiConstants::MESSAGE_TYPE_SUBMISSIONREVIEW;
}
public function validate(array $jwtBody)
public static function validate(array $jwtBody): void
{
if (empty($jwtBody['sub'])) {
throw new LtiException('Must have a user (sub)');
}
if ($jwtBody[LtiConstants::VERSION] !== LtiConstants::V1_3) {
throw new LtiException('Incorrect version, expected 1.3.0');
}
if (!isset($jwtBody[LtiConstants::ROLES])) {
throw new LtiException('Missing Roles Claim');
}
static::validateGenericMessage($jwtBody);
if (empty($jwtBody[LtiConstants::RESOURCE_LINK]['id'])) {
throw new LtiException('Missing Resource Link Id');
}
if (empty($jwtBody[LtiConstants::FOR_USER])) {
throw new LtiException('Missing For User');
}
return true;
}
}