MDL-79675 libraries: upgrade lti1p3 to v6.0.0
This commit is contained in:
@@ -4,12 +4,10 @@ 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.4.1 of the packbackbooks/lti-1-3-php-library](https://github.com/packbackbooks/lti-1-3-php-library/releases/tag/v5.4.1) library.
|
||||
It is currently based on version [6.0.0 of the packbackbooks/lti-1-3-php-library](https://github.com/packbackbooks/lti-1-3-php-library/releases/tag/v6.0.0) 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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3\Concerns;
|
||||
|
||||
use Packback\Lti1p3\Helpers\Helpers;
|
||||
|
||||
trait Arrayable
|
||||
{
|
||||
abstract public function getArray(): array;
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return Helpers::filterOutNulls($this->getArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3\Concerns;
|
||||
|
||||
trait JsonStringable
|
||||
{
|
||||
use Arrayable;
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return json_encode($this->toArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3\DeepLinkResources;
|
||||
|
||||
use DateTime;
|
||||
use Packback\Lti1p3\Concerns\Arrayable;
|
||||
use Packback\Lti1p3\LtiException;
|
||||
|
||||
class DateTimeInterval
|
||||
{
|
||||
use Arrayable;
|
||||
public const ERROR_NO_START_OR_END = 'Either a start or end time must be specified.';
|
||||
public const ERROR_START_GT_END = 'The start time cannot be greater than end time.';
|
||||
|
||||
public function __construct(
|
||||
private ?DateTime $start = null,
|
||||
private ?DateTime $end = null
|
||||
) {
|
||||
$this->validateStartAndEnd();
|
||||
}
|
||||
|
||||
public static function new(): self
|
||||
{
|
||||
return new DateTimeInterval();
|
||||
}
|
||||
|
||||
public function getArray(): array
|
||||
{
|
||||
if (!isset($this->start) && !isset($this->end)) {
|
||||
throw new LtiException(self::ERROR_NO_START_OR_END);
|
||||
}
|
||||
|
||||
$this->validateStartAndEnd();
|
||||
|
||||
return [
|
||||
'startDateTime' => $this->start?->format(DateTime::ATOM),
|
||||
'endDateTime' => $this->end?->format(DateTime::ATOM),
|
||||
];
|
||||
}
|
||||
|
||||
public function setStart(?DateTime $start): self
|
||||
{
|
||||
$this->start = $start;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStart(): ?DateTime
|
||||
{
|
||||
return $this->start;
|
||||
}
|
||||
|
||||
public function setEnd(?DateTime $end): self
|
||||
{
|
||||
$this->end = $end;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEnd(): ?DateTime
|
||||
{
|
||||
return $this->end;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LtiException
|
||||
*/
|
||||
private function validateStartAndEnd(): void
|
||||
{
|
||||
if (isset($this->start) && isset($this->end) && $this->start > $this->end) {
|
||||
throw new LtiException(self::ERROR_START_GT_END);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3\DeepLinkResources;
|
||||
|
||||
trait HasDimensions
|
||||
{
|
||||
public function setWidth(?int $width): self
|
||||
{
|
||||
$this->width = $width;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWidth(): ?int
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
public function setHeight(?int $height): self
|
||||
{
|
||||
$this->height = $height;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHeight(): ?int
|
||||
{
|
||||
return $this->height;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3\DeepLinkResources;
|
||||
|
||||
use Packback\Lti1p3\Concerns\Arrayable;
|
||||
|
||||
class Icon
|
||||
{
|
||||
use Arrayable, HasDimensions;
|
||||
|
||||
public function __construct(
|
||||
private string $url,
|
||||
private int $width,
|
||||
private int $height
|
||||
) {
|
||||
}
|
||||
|
||||
public static function new(string $url, int $width, int $height): self
|
||||
{
|
||||
return new Icon($url, $width, $height);
|
||||
}
|
||||
|
||||
public function getArray(): array
|
||||
{
|
||||
return [
|
||||
'url' => $this->url,
|
||||
'width' => $this->width,
|
||||
'height' => $this->height,
|
||||
];
|
||||
}
|
||||
|
||||
public function setUrl(string $url): self
|
||||
{
|
||||
$this->url = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUrl(): string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3\DeepLinkResources;
|
||||
|
||||
use Packback\Lti1p3\Concerns\Arrayable;
|
||||
|
||||
class Iframe
|
||||
{
|
||||
use Arrayable, HasDimensions;
|
||||
|
||||
public function __construct(
|
||||
private ?string $src = null,
|
||||
private ?int $width = null,
|
||||
private ?int $height = null
|
||||
) {
|
||||
}
|
||||
|
||||
public static function new(): self
|
||||
{
|
||||
return new Iframe();
|
||||
}
|
||||
|
||||
public function getArray(): array
|
||||
{
|
||||
return [
|
||||
'width' => $this->width,
|
||||
'height' => $this->height,
|
||||
'src' => $this->src,
|
||||
];
|
||||
}
|
||||
|
||||
public function setSrc(?string $src): self
|
||||
{
|
||||
$this->src = $src;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSrc(): ?string
|
||||
{
|
||||
return $this->src;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3\DeepLinkResources;
|
||||
|
||||
use Packback\Lti1p3\Concerns\Arrayable;
|
||||
use Packback\Lti1p3\LtiConstants;
|
||||
use Packback\Lti1p3\LtiLineitem;
|
||||
|
||||
class Resource
|
||||
{
|
||||
use Arrayable;
|
||||
private string $type = LtiConstants::DL_RESOURCE_LINK_TYPE;
|
||||
private ?string $title = null;
|
||||
private ?string $text = null;
|
||||
private ?string $url = null;
|
||||
private ?LtiLineitem $line_item = null;
|
||||
private ?Icon $icon = null;
|
||||
private ?Icon $thumbnail = null;
|
||||
private array $custom_params = [];
|
||||
private string $target = 'iframe';
|
||||
private ?Iframe $iframe = null;
|
||||
private ?Window $window = null;
|
||||
private ?DateTimeInterval $availability_interval = null;
|
||||
private ?DateTimeInterval $submission_interval = null;
|
||||
|
||||
public static function new(): self
|
||||
{
|
||||
return new Resource();
|
||||
}
|
||||
|
||||
public function getArray(): array
|
||||
{
|
||||
$resource = [
|
||||
'type' => $this->type,
|
||||
'title' => $this->title,
|
||||
'text' => $this->text,
|
||||
'url' => $this->url,
|
||||
'icon' => $this->icon?->toArray(),
|
||||
'thumbnail' => $this->thumbnail?->toArray(),
|
||||
'iframe' => $this->iframe?->toArray(),
|
||||
'window' => $this->window?->toArray(),
|
||||
'available' => $this->availability_interval?->toArray(),
|
||||
'submission' => $this->submission_interval?->toArray(),
|
||||
];
|
||||
|
||||
if (!empty($this->custom_params)) {
|
||||
$resource['custom'] = $this->custom_params;
|
||||
}
|
||||
|
||||
if (isset($this->line_item)) {
|
||||
$resource['lineItem'] = [
|
||||
'scoreMaximum' => $this->line_item->getScoreMaximum(),
|
||||
'label' => $this->line_item->getLabel(),
|
||||
];
|
||||
}
|
||||
|
||||
// Kept for backwards compatibility
|
||||
if (!isset($this->iframe) && !isset($this->window)) {
|
||||
$resource['presentation'] = [
|
||||
'documentTarget' => $this->target,
|
||||
];
|
||||
}
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(string $value): self
|
||||
{
|
||||
$this->type = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(?string $value): self
|
||||
{
|
||||
$this->title = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getText(): ?string
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
public function setText(?string $value): self
|
||||
{
|
||||
$this->text = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUrl(): ?string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
public function setUrl(?string $value): self
|
||||
{
|
||||
$this->url = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLineItem(): ?LtiLineitem
|
||||
{
|
||||
return $this->line_item;
|
||||
}
|
||||
|
||||
public function setLineItem(?LtiLineitem $value): self
|
||||
{
|
||||
$this->line_item = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setIcon(?Icon $icon): self
|
||||
{
|
||||
$this->icon = $icon;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIcon(): ?Icon
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function setThumbnail(?Icon $thumbnail): self
|
||||
{
|
||||
$this->thumbnail = $thumbnail;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getThumbnail(): ?Icon
|
||||
{
|
||||
return $this->thumbnail;
|
||||
}
|
||||
|
||||
public function getCustomParams(): array
|
||||
{
|
||||
return $this->custom_params;
|
||||
}
|
||||
|
||||
public function setCustomParams(array $value): self
|
||||
{
|
||||
$this->custom_params = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIframe(): ?Iframe
|
||||
{
|
||||
return $this->iframe;
|
||||
}
|
||||
|
||||
public function setIframe(?Iframe $iframe): self
|
||||
{
|
||||
$this->iframe = $iframe;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWindow(): ?Window
|
||||
{
|
||||
return $this->window;
|
||||
}
|
||||
|
||||
public function setWindow(?Window $window): self
|
||||
{
|
||||
$this->window = $window;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAvailabilityInterval(): ?DateTimeInterval
|
||||
{
|
||||
return $this->availability_interval;
|
||||
}
|
||||
|
||||
public function setAvailabilityInterval(?DateTimeInterval $availabilityInterval): self
|
||||
{
|
||||
$this->availability_interval = $availabilityInterval;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSubmissionInterval(): ?DateTimeInterval
|
||||
{
|
||||
return $this->submission_interval;
|
||||
}
|
||||
|
||||
public function setSubmissionInterval(?DateTimeInterval $submissionInterval): self
|
||||
{
|
||||
$this->submission_interval = $submissionInterval;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3\DeepLinkResources;
|
||||
|
||||
use Packback\Lti1p3\Concerns\Arrayable;
|
||||
|
||||
class Window
|
||||
{
|
||||
use Arrayable, HasDimensions;
|
||||
|
||||
public function __construct(
|
||||
private ?string $target_name = null,
|
||||
private ?int $width = null,
|
||||
private ?int $height = null,
|
||||
private ?string $window_features = null
|
||||
) {
|
||||
}
|
||||
|
||||
public static function new(): self
|
||||
{
|
||||
return new Window();
|
||||
}
|
||||
|
||||
public function getArray(): array
|
||||
{
|
||||
return [
|
||||
'targetName' => $this->target_name,
|
||||
'width' => $this->width,
|
||||
'height' => $this->height,
|
||||
'windowFeatures' => $this->window_features,
|
||||
];
|
||||
}
|
||||
|
||||
public function setTargetName(?string $targetName): self
|
||||
{
|
||||
$this->target_name = $targetName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTargetName(): ?string
|
||||
{
|
||||
return $this->target_name;
|
||||
}
|
||||
|
||||
public function setWindowFeatures(?string $windowFeatures): self
|
||||
{
|
||||
$this->window_features = $windowFeatures;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWindowFeatures(): ?string
|
||||
{
|
||||
return $this->window_features;
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,23 @@ namespace Packback\Lti1p3\Helpers;
|
||||
|
||||
class Helpers
|
||||
{
|
||||
public static function checkIfNullValue($value): bool
|
||||
public static function filterOutNulls(array $array): array
|
||||
{
|
||||
return !is_null($value);
|
||||
return array_filter($array, fn ($value) => !is_null($value));
|
||||
}
|
||||
|
||||
public static function buildUrlWithQueryParams(string $url, array $params = []): string
|
||||
{
|
||||
if (empty($params)) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
if (parse_url($url, PHP_URL_QUERY)) {
|
||||
$separator = '&';
|
||||
} else {
|
||||
$separator = '?';
|
||||
}
|
||||
|
||||
return $url.$separator.http_build_query($params, '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3\ImsStorage;
|
||||
|
||||
use Packback\Lti1p3\Interfaces\ICache;
|
||||
|
||||
class ImsCache implements ICache
|
||||
{
|
||||
private $cache;
|
||||
|
||||
public function getLaunchData(string $key): ?array
|
||||
{
|
||||
$this->loadCache();
|
||||
|
||||
return $this->cache[$key] ?? null;
|
||||
}
|
||||
|
||||
public function cacheLaunchData(string $key, array $jwtBody): void
|
||||
{
|
||||
$this->loadCache();
|
||||
|
||||
$this->cache[$key] = $jwtBody;
|
||||
$this->saveCache();
|
||||
}
|
||||
|
||||
public function cacheNonce(string $nonce, string $state): void
|
||||
{
|
||||
$this->loadCache();
|
||||
|
||||
$this->cache['nonce'][$nonce] = $state;
|
||||
$this->saveCache();
|
||||
}
|
||||
|
||||
public function checkNonceIsValid(string $nonce, string $state): bool
|
||||
{
|
||||
$this->loadCache();
|
||||
|
||||
return isset($this->cache['nonce'][$nonce]) &&
|
||||
$this->cache['nonce'][$nonce] === $state;
|
||||
}
|
||||
|
||||
public function cacheAccessToken(string $key, string $accessToken): void
|
||||
{
|
||||
$this->loadCache();
|
||||
|
||||
$this->cache[$key] = $accessToken;
|
||||
$this->saveCache();
|
||||
}
|
||||
|
||||
public function getAccessToken(string $key): ?string
|
||||
{
|
||||
$this->loadCache();
|
||||
|
||||
return $this->cache[$key] ?? null;
|
||||
}
|
||||
|
||||
public function clearAccessToken(string $key): void
|
||||
{
|
||||
$this->loadCache();
|
||||
|
||||
unset($this->cache[$key]);
|
||||
$this->saveCache();
|
||||
}
|
||||
|
||||
private function loadCache()
|
||||
{
|
||||
$cache = file_get_contents(sys_get_temp_dir().'/lti_cache.txt');
|
||||
if (empty($cache)) {
|
||||
file_put_contents(sys_get_temp_dir().'/lti_cache.txt', '{}');
|
||||
$this->cache = [];
|
||||
}
|
||||
$this->cache = json_decode($cache, true);
|
||||
}
|
||||
|
||||
private function saveCache()
|
||||
{
|
||||
file_put_contents(sys_get_temp_dir().'/lti_cache.txt', json_encode($this->cache));
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3\ImsStorage;
|
||||
|
||||
use Packback\Lti1p3\Interfaces\ICookie;
|
||||
|
||||
class ImsCookie implements ICookie
|
||||
{
|
||||
public function getCookie(string $name): ?string
|
||||
{
|
||||
if (isset($_COOKIE[$name])) {
|
||||
return $_COOKIE[$name];
|
||||
}
|
||||
// Look for backup cookie if same site is not supported by the user's browser.
|
||||
if (isset($_COOKIE['LEGACY_'.$name])) {
|
||||
return $_COOKIE['LEGACY_'.$name];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function setCookie(string $name, string $value, $exp = 3600, $options = []): void
|
||||
{
|
||||
$cookie_options = [
|
||||
'expires' => time() + $exp,
|
||||
];
|
||||
|
||||
// SameSite none and secure will be required for tools to work inside iframes
|
||||
$same_site_options = [
|
||||
'samesite' => 'None',
|
||||
'secure' => true,
|
||||
];
|
||||
|
||||
setcookie($name, $value, array_merge($cookie_options, $same_site_options, $options));
|
||||
|
||||
// Set a second fallback cookie in the event that "SameSite" is not supported
|
||||
setcookie('LEGACY_'.$name, $value, array_merge($cookie_options, $options));
|
||||
}
|
||||
}
|
||||
@@ -6,5 +6,5 @@ interface ICookie
|
||||
{
|
||||
public function getCookie(string $name): ?string;
|
||||
|
||||
public function setCookie(string $name, string $value, $exp = 3600, $options = []): void;
|
||||
public function setCookie(string $name, string $value, int $exp = 3600, array $options = []): void;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace Packback\Lti1p3\Interfaces;
|
||||
|
||||
interface IDatabase
|
||||
{
|
||||
public function findRegistrationByIssuer($iss, $clientId = null);
|
||||
public function findRegistrationByIssuer(string $iss, ?string $clientId = null): ?ILtiRegistration;
|
||||
|
||||
public function findDeployment($iss, $deploymentId, $clientId = null);
|
||||
public function findDeployment(string $iss, string $deploymentId, ?string $clientId = null): ?ILtiDeployment;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3\Interfaces;
|
||||
|
||||
/** @internal */
|
||||
interface ILtiDeployment
|
||||
{
|
||||
public function getDeploymentId();
|
||||
|
||||
public function setDeploymentId($deployment_id): ILtiDeployment;
|
||||
}
|
||||
@@ -7,33 +7,33 @@ interface ILtiRegistration
|
||||
{
|
||||
public function getIssuer();
|
||||
|
||||
public function setIssuer($issuer);
|
||||
public function setIssuer(string $issuer): ILtiRegistration;
|
||||
|
||||
public function getClientId();
|
||||
|
||||
public function setClientId($clientId);
|
||||
public function setClientId(string $clientId): ILtiRegistration;
|
||||
|
||||
public function getKeySetUrl();
|
||||
public function getKeySetUrl(): ?string;
|
||||
|
||||
public function setKeySetUrl($keySetUrl);
|
||||
public function setKeySetUrl(string $keySetUrl): ILtiRegistration;
|
||||
|
||||
public function getAuthTokenUrl();
|
||||
public function getAuthTokenUrl(): ?string;
|
||||
|
||||
public function setAuthTokenUrl($authTokenUrl);
|
||||
public function setAuthTokenUrl(?string $authTokenUrl): ILtiRegistration;
|
||||
|
||||
public function getAuthLoginUrl();
|
||||
public function getAuthLoginUrl(): ?string;
|
||||
|
||||
public function setAuthLoginUrl($authLoginUrl);
|
||||
public function setAuthLoginUrl(string $authLoginUrl): ILtiRegistration;
|
||||
|
||||
public function getAuthServer();
|
||||
public function getAuthServer(): ?string;
|
||||
|
||||
public function setAuthServer($authServer);
|
||||
public function setAuthServer(string $authServer): ILtiRegistration;
|
||||
|
||||
public function getToolPrivateKey();
|
||||
|
||||
public function setToolPrivateKey($toolPrivateKey);
|
||||
public function setToolPrivateKey(string $toolPrivateKey): ILtiRegistration;
|
||||
|
||||
public function getKid();
|
||||
|
||||
public function setKid($kid);
|
||||
public function setKid(string $kid): ILtiRegistration;
|
||||
}
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
|
||||
namespace Packback\Lti1p3\Interfaces;
|
||||
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/** @internal */
|
||||
interface ILtiServiceConnector
|
||||
{
|
||||
public function getAccessToken(ILtiRegistration $registration, array $scopes);
|
||||
public function getAccessToken(ILtiRegistration $registration, array $scopes): string;
|
||||
|
||||
public function makeRequest(IServiceRequest $request);
|
||||
public function makeRequest(IServiceRequest $request): ResponseInterface;
|
||||
|
||||
public function getResponseBody(Response $request): ?array;
|
||||
public function getResponseBody(ResponseInterface $response): ?array;
|
||||
|
||||
public function getResponseHeaders(ResponseInterface $response): ?array;
|
||||
|
||||
public function makeServiceRequest(
|
||||
ILtiRegistration $registration,
|
||||
@@ -23,8 +26,8 @@ interface ILtiServiceConnector
|
||||
ILtiRegistration $registration,
|
||||
array $scopes,
|
||||
IServiceRequest $request,
|
||||
string $key
|
||||
?string $key
|
||||
): array;
|
||||
|
||||
public function setDebuggingMode(bool $enable): void;
|
||||
public function setDebuggingMode(bool $enable): ILtiServiceConnector;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3\Interfaces;
|
||||
|
||||
use Packback\Lti1p3\LtiMessageLaunch;
|
||||
|
||||
/**
|
||||
* This is an optional interface if an LTI 1.3 tool supports migrations
|
||||
* from LTI 1.1 compatible installations.
|
||||
*
|
||||
* To use this, just have whatever class you create that implements IDatabase
|
||||
* also implement this interface.
|
||||
*/
|
||||
interface IMigrationDatabase extends IDatabase
|
||||
{
|
||||
/**
|
||||
* Using the LtiMessageLaunch return an array of matching LTI 1.1 keys
|
||||
*
|
||||
* @return array<\Packback\Lti1p3\Lti1p1Key>
|
||||
*/
|
||||
public function findLti1p1Keys(LtiMessageLaunch $launch): array;
|
||||
|
||||
/**
|
||||
* Given an LtiMessageLaunch, return true if this tool should migrate from 1.1 to 1.3
|
||||
*/
|
||||
public function shouldMigrate(LtiMessageLaunch $launch): bool;
|
||||
|
||||
/**
|
||||
* This method should create a 1.3 deployment in your DB based on the LtiMessageLaunch.
|
||||
* Previous to this, we validated the oauth_consumer_key_sign to ensure this migration
|
||||
* can safely occur.
|
||||
*/
|
||||
public function migrateFromLti1p1(LtiMessageLaunch $launch): ?ILtiDeployment;
|
||||
}
|
||||
@@ -11,15 +11,21 @@ interface IServiceRequest
|
||||
|
||||
public function getPayload(): array;
|
||||
|
||||
public function setUrl(string $url): self;
|
||||
public function setUrl(string $url): IServiceRequest;
|
||||
|
||||
public function setAccessToken(string $accessToken): self;
|
||||
public function setAccessToken(string $accessToken): IServiceRequest;
|
||||
|
||||
public function setBody(string $body): self;
|
||||
public function setBody(string $body): IServiceRequest;
|
||||
|
||||
public function setAccept(string $accept): self;
|
||||
public function setPayload(array $payload): IServiceRequest;
|
||||
|
||||
public function setContentType(string $contentType): self;
|
||||
public function setAccept(string $accept): IServiceRequest;
|
||||
|
||||
public function setContentType(string $contentType): IServiceRequest;
|
||||
|
||||
public function getErrorPrefix(): string;
|
||||
|
||||
public function getMaskResponseLogs(): bool;
|
||||
|
||||
public function setMaskResponseLogs(bool $maskResponseLogs): self;
|
||||
}
|
||||
|
||||
@@ -8,31 +8,28 @@ use Packback\Lti1p3\Interfaces\ILtiRegistration;
|
||||
|
||||
class JwksEndpoint
|
||||
{
|
||||
private $keys;
|
||||
|
||||
public function __construct(array $keys)
|
||||
public function __construct(private array $keys)
|
||||
{
|
||||
$this->keys = $keys;
|
||||
}
|
||||
|
||||
public static function new(array $keys)
|
||||
public static function new(array $keys): self
|
||||
{
|
||||
return new JwksEndpoint($keys);
|
||||
}
|
||||
|
||||
public static function fromIssuer(IDatabase $database, $issuer)
|
||||
public static function fromIssuer(IDatabase $database, string $issuer): self
|
||||
{
|
||||
$registration = $database->findRegistrationByIssuer($issuer);
|
||||
|
||||
return new JwksEndpoint([$registration->getKid() => $registration->getToolPrivateKey()]);
|
||||
}
|
||||
|
||||
public static function fromRegistration(ILtiRegistration $registration)
|
||||
public static function fromRegistration(ILtiRegistration $registration): self
|
||||
{
|
||||
return new JwksEndpoint([$registration->getKid() => $registration->getToolPrivateKey()]);
|
||||
}
|
||||
|
||||
public function getPublicJwks()
|
||||
public function getPublicJwks(): array
|
||||
{
|
||||
$jwks = [];
|
||||
foreach ($this->keys as $kid => $private_key) {
|
||||
@@ -51,9 +48,4 @@ class JwksEndpoint
|
||||
|
||||
return ['keys' => $jwks];
|
||||
}
|
||||
|
||||
public function outputJwks()
|
||||
{
|
||||
echo json_encode($this->getPublicJwks());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3;
|
||||
|
||||
/**
|
||||
* Used for migrations from LTI 1.1 to LTI 1.3
|
||||
*
|
||||
* @see IMigrationDatabase
|
||||
*/
|
||||
class Lti1p1Key
|
||||
{
|
||||
private ?string $key;
|
||||
private ?string $secret;
|
||||
|
||||
public function __construct(?array $key = null)
|
||||
{
|
||||
$this->key = $key['key'] ?? null;
|
||||
$this->secret = $key['secret'] ?? null;
|
||||
}
|
||||
|
||||
public function getKey(): ?string
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function setKey(string $key): self
|
||||
{
|
||||
$this->key = $key;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSecret(): ?string
|
||||
{
|
||||
return $this->secret;
|
||||
}
|
||||
|
||||
public function setSecret(string $secret): self
|
||||
{
|
||||
$this->secret = $secret;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a signature using the key and secret
|
||||
*
|
||||
* @see https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key_sign
|
||||
*/
|
||||
public function sign(string $deploymentId, string $iss, string $clientId, string $exp, string $nonce): string
|
||||
{
|
||||
$signatureComponents = [
|
||||
$this->getKey(),
|
||||
$deploymentId,
|
||||
$iss,
|
||||
$clientId,
|
||||
$exp,
|
||||
$nonce,
|
||||
];
|
||||
|
||||
$baseString = implode('&', $signatureComponents);
|
||||
$utf8String = mb_convert_encoding($baseString, 'utf8', mb_detect_encoding($baseString));
|
||||
$hash = hash_hmac('sha256', $utf8String, $this->getSecret(), true);
|
||||
|
||||
return base64_encode($hash);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,18 +8,11 @@ use Packback\Lti1p3\Interfaces\IServiceRequest;
|
||||
|
||||
abstract class LtiAbstractService
|
||||
{
|
||||
private $serviceConnector;
|
||||
private $registration;
|
||||
private $serviceData;
|
||||
|
||||
public function __construct(
|
||||
ILtiServiceConnector $serviceConnector,
|
||||
ILtiRegistration $registration,
|
||||
array $serviceData
|
||||
private ILtiServiceConnector $serviceConnector,
|
||||
private ILtiRegistration $registration,
|
||||
private array $serviceData
|
||||
) {
|
||||
$this->serviceConnector = $serviceConnector;
|
||||
$this->registration = $registration;
|
||||
$this->serviceData = $serviceData;
|
||||
}
|
||||
|
||||
public function getServiceData(): array
|
||||
@@ -39,7 +32,7 @@ abstract class LtiAbstractService
|
||||
protected function validateScopes(array $scopes): void
|
||||
{
|
||||
if (empty(array_intersect($scopes, $this->getScope()))) {
|
||||
throw new LtiException('Missing required scope', 1);
|
||||
throw new LtiException('Missing required scope');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +45,7 @@ abstract class LtiAbstractService
|
||||
);
|
||||
}
|
||||
|
||||
protected function getAll(IServiceRequest $request, string $key = null): array
|
||||
protected function getAll(IServiceRequest $request, ?string $key = null): array
|
||||
{
|
||||
return $this->serviceConnector->getAll(
|
||||
$this->registration,
|
||||
|
||||
@@ -28,17 +28,12 @@ class LtiAssignmentsGradesService extends LtiAbstractService
|
||||
return LtiLineitem::new()->setId($serviceData['lineitem']);
|
||||
}
|
||||
|
||||
public function putGrade(LtiGrade $grade, LtiLineitem $lineitem = null)
|
||||
public function putGrade(LtiGrade $grade, ?LtiLineitem $lineitem = null)
|
||||
{
|
||||
$this->validateScopes([LtiConstants::AGS_SCOPE_SCORE]);
|
||||
|
||||
$lineitem = $this->ensureLineItemExists($lineitem);
|
||||
|
||||
$scoreUrl = $lineitem->getId();
|
||||
|
||||
// Place '/scores' before url params
|
||||
$pos = strpos($scoreUrl, '?');
|
||||
$scoreUrl = $pos === false ? $scoreUrl.'/scores' : substr_replace($scoreUrl, '/scores', $pos, 0);
|
||||
$scoreUrl = $this->appendLineItemPath($lineitem, '/scores');
|
||||
|
||||
$request = new ServiceRequest(
|
||||
ServiceRequest::METHOD_POST,
|
||||
@@ -64,11 +59,11 @@ class LtiAssignmentsGradesService extends LtiAbstractService
|
||||
return null;
|
||||
}
|
||||
|
||||
public function updateLineitem(LtiLineItem $lineitemToUpdate): LtiLineitem
|
||||
public function updateLineitem(LtiLineitem $lineitemToUpdate): LtiLineitem
|
||||
{
|
||||
$request = new ServiceRequest(
|
||||
ServiceRequest::METHOD_PUT,
|
||||
$this->getServiceData()['lineitem'],
|
||||
$lineitemToUpdate->getId(),
|
||||
ServiceRequest::TYPE_UPDATE_LINEITEM
|
||||
);
|
||||
|
||||
@@ -112,14 +107,10 @@ class LtiAssignmentsGradesService extends LtiAbstractService
|
||||
return $this->findLineItem($newLineItem) ?? $this->createLineitem($newLineItem);
|
||||
}
|
||||
|
||||
public function getGrades(LtiLineitem $lineitem = null)
|
||||
public function getGrades(?LtiLineitem $lineitem = null)
|
||||
{
|
||||
$lineitem = $this->ensureLineItemExists($lineitem);
|
||||
$resultsUrl = $lineitem->getId();
|
||||
|
||||
// Place '/results' before url params
|
||||
$pos = strpos($resultsUrl, '?');
|
||||
$resultsUrl = $pos === false ? $resultsUrl.'/results' : substr_replace($resultsUrl, '/results', $pos, 0);
|
||||
$resultsUrl = $this->appendLineItemPath($lineitem, '/results');
|
||||
|
||||
$request = new ServiceRequest(
|
||||
ServiceRequest::METHOD_GET,
|
||||
@@ -168,7 +159,7 @@ class LtiAssignmentsGradesService extends LtiAbstractService
|
||||
return new LtiLineitem($response);
|
||||
}
|
||||
|
||||
private function ensureLineItemExists(LtiLineitem $lineitem = null): LtiLineitem
|
||||
private function ensureLineItemExists(?LtiLineitem $lineitem = null): LtiLineitem
|
||||
{
|
||||
// If no line item is passed in, attempt to use the one associated with
|
||||
// this launch.
|
||||
@@ -198,4 +189,18 @@ class LtiAssignmentsGradesService extends LtiAbstractService
|
||||
$newLineItem->getResourceId() == ($lineitem['resourceId'] ?? null) &&
|
||||
$newLineItem->getResourceLinkId() == ($lineitem['resourceLinkId'] ?? null);
|
||||
}
|
||||
|
||||
private function appendLineItemPath(LtiLineitem $lineItem, string $suffix): string
|
||||
{
|
||||
$url = $lineItem->getId();
|
||||
$pos = strpos($url, '?');
|
||||
|
||||
if ($pos === false) {
|
||||
$url = $url.$suffix;
|
||||
} else {
|
||||
$url = substr_replace($url, $suffix, $pos, 0);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,12 @@ class LtiConstants
|
||||
|
||||
// Optional message claims
|
||||
public const CONTEXT = 'https://purl.imsglobal.org/spec/lti/claim/context';
|
||||
public const TOOL_PLATFORM = 'https://purl.imsglobal.org/spec/lti/claim/tool_platform';
|
||||
public const ROLE_SCOPE_MENTOR = 'https://purlimsglobal.org/spec/lti/claim/role_scope_mentor';
|
||||
public const CUSTOM = 'https://purl.imsglobal.org/spec/lti/claim/custom';
|
||||
public const LAUNCH_PRESENTATION = 'https://purl.imsglobal.org/spec/lti/claim/launch_presentation';
|
||||
public const LIS = 'https://purl.imsglobal.org/spec/lti/claim/lis';
|
||||
public const CUSTOM = 'https://purl.imsglobal.org/spec/lti/claim/custom';
|
||||
public const LTI1P1 = 'https://purl.imsglobal.org/spec/lti/claim/lti1p1';
|
||||
public const ROLE_SCOPE_MENTOR = 'https://purlimsglobal.org/spec/lti/claim/role_scope_mentor';
|
||||
public const TOOL_PLATFORM = 'https://purl.imsglobal.org/spec/lti/claim/tool_platform';
|
||||
|
||||
// LTI DL
|
||||
public const DL_CONTENT_ITEMS = 'https://purl.imsglobal.org/spec/lti-dl/claim/content_items';
|
||||
|
||||
@@ -40,7 +40,7 @@ class LtiCourseGroupsService extends LtiAbstractService
|
||||
return $this->getAll($request, 'sets');
|
||||
}
|
||||
|
||||
public function getGroupsBySet()
|
||||
public function getGroupsBySet(): array
|
||||
{
|
||||
$groups = $this->getGroups();
|
||||
$sets = $this->getSets();
|
||||
|
||||
@@ -7,18 +7,14 @@ use Packback\Lti1p3\Interfaces\ILtiRegistration;
|
||||
|
||||
class LtiDeepLink
|
||||
{
|
||||
private $registration;
|
||||
private $deployment_id;
|
||||
private $deep_link_settings;
|
||||
|
||||
public function __construct(ILtiRegistration $registration, string $deployment_id, array $deep_link_settings)
|
||||
{
|
||||
$this->registration = $registration;
|
||||
$this->deployment_id = $deployment_id;
|
||||
$this->deep_link_settings = $deep_link_settings;
|
||||
public function __construct(
|
||||
private ILtiRegistration $registration,
|
||||
private string $deployment_id,
|
||||
private array $deep_link_settings
|
||||
) {
|
||||
}
|
||||
|
||||
public function getResponseJwt($resources)
|
||||
public function getResponseJwt(array $resources): string
|
||||
{
|
||||
$message_jwt = [
|
||||
'iss' => $this->registration->getClientId(),
|
||||
@@ -42,28 +38,4 @@ class LtiDeepLink
|
||||
|
||||
return JWT::encode($message_jwt, $this->registration->getToolPrivateKey(), 'RS256', $this->registration->getKid());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method builds an auto-submitting HTML form to post the deep linking response message
|
||||
* back to platform, as per LTI-DL 2.0 specification. The resulting HTML is then written to standard output,
|
||||
* so calling this method will automatically send an HTTP response to conclude the content selection flow.
|
||||
*
|
||||
* @param LtiDeepLinkResource[] $resources The list of selected resources to be sent to the platform
|
||||
*
|
||||
* @todo Consider wrapping the content inside a well-formed HTML document,
|
||||
* and returning it instead of directly writing to standard output
|
||||
*/
|
||||
public function outputResponseForm($resources)
|
||||
{
|
||||
$jwt = $this->getResponseJwt($resources);
|
||||
$formActionUrl = $this->deep_link_settings['deep_link_return_url'];
|
||||
|
||||
echo <<<HTML
|
||||
<form id="auto_submit" action="{$formActionUrl}" method="POST">
|
||||
<input type="hidden" name="JWT" value="{$jwt}" />
|
||||
<input type="submit" name="Go" />
|
||||
</form>
|
||||
<script>document.getElementById('auto_submit').submit();</script>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3;
|
||||
|
||||
use DateTime;
|
||||
|
||||
class LtiDeepLinkDateTimeInterval
|
||||
{
|
||||
private ?DateTime $start;
|
||||
private ?DateTime $end;
|
||||
|
||||
public function __construct(DateTime $start = null, DateTime $end = null)
|
||||
{
|
||||
if ($start !== null && $end !== null && $end < $start) {
|
||||
throw new LtiException('Interval start time cannot be greater than end time');
|
||||
}
|
||||
|
||||
$this->start = $start ?? null;
|
||||
$this->end = $end ?? null;
|
||||
}
|
||||
|
||||
public static function new(): LtiDeepLinkDateTimeInterval
|
||||
{
|
||||
return new LtiDeepLinkDateTimeInterval();
|
||||
}
|
||||
|
||||
public function setStart(?DateTime $start): LtiDeepLinkDateTimeInterval
|
||||
{
|
||||
$this->start = $start;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStart(): ?DateTime
|
||||
{
|
||||
return $this->start;
|
||||
}
|
||||
|
||||
public function setEnd(?DateTime $end): LtiDeepLinkDateTimeInterval
|
||||
{
|
||||
$this->end = $end;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEnd(): ?DateTime
|
||||
{
|
||||
return $this->end;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
if (!isset($this->start) && !isset($this->end)) {
|
||||
throw new LtiException('At least one of the interval bounds must be specified on the object instance');
|
||||
}
|
||||
|
||||
if ($this->start !== null && $this->end !== null && $this->end < $this->start) {
|
||||
throw new LtiException('Interval start time cannot be greater than end time');
|
||||
}
|
||||
|
||||
$dateTimeInterval = [];
|
||||
|
||||
if (isset($this->start)) {
|
||||
$dateTimeInterval['startDateTime'] = $this->start->format(DateTime::ATOM);
|
||||
}
|
||||
if (isset($this->end)) {
|
||||
$dateTimeInterval['endDateTime'] = $this->end->format(DateTime::ATOM);
|
||||
}
|
||||
|
||||
return $dateTimeInterval;
|
||||
}
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3;
|
||||
|
||||
class LtiDeepLinkResource
|
||||
{
|
||||
private $type = LtiConstants::DL_RESOURCE_LINK_TYPE;
|
||||
private $title;
|
||||
private $text;
|
||||
private $url;
|
||||
private $line_item;
|
||||
private $icon;
|
||||
private $thumbnail;
|
||||
private $custom_params = [];
|
||||
private $target = 'iframe';
|
||||
private $iframe;
|
||||
private $window;
|
||||
private $availability_interval;
|
||||
private $submission_interval;
|
||||
|
||||
public static function new(): LtiDeepLinkResource
|
||||
{
|
||||
return new LtiDeepLinkResource();
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(string $value): LtiDeepLinkResource
|
||||
{
|
||||
$this->type = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(?string $value): LtiDeepLinkResource
|
||||
{
|
||||
$this->title = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getText(): ?string
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
public function setText(?string $value): LtiDeepLinkResource
|
||||
{
|
||||
$this->text = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUrl(): ?string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
public function setUrl(?string $value): LtiDeepLinkResource
|
||||
{
|
||||
$this->url = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLineItem(): ?LtiLineitem
|
||||
{
|
||||
return $this->line_item;
|
||||
}
|
||||
|
||||
public function setLineItem(?LtiLineitem $value): LtiDeepLinkResource
|
||||
{
|
||||
$this->line_item = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setIcon(?LtiDeepLinkResourceIcon $icon): LtiDeepLinkResource
|
||||
{
|
||||
$this->icon = $icon;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIcon(): ?LtiDeepLinkResourceIcon
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function setThumbnail(?LtiDeepLinkResourceIcon $thumbnail): LtiDeepLinkResource
|
||||
{
|
||||
$this->thumbnail = $thumbnail;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getThumbnail(): ?LtiDeepLinkResourceIcon
|
||||
{
|
||||
return $this->thumbnail;
|
||||
}
|
||||
|
||||
public function getCustomParams(): array
|
||||
{
|
||||
return $this->custom_params;
|
||||
}
|
||||
|
||||
public function setCustomParams(array $value): LtiDeepLinkResource
|
||||
{
|
||||
$this->custom_params = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This field maps the "presentation" resource property, which is non-standard.
|
||||
* Consider using "iframe" and/or "window" instead.
|
||||
*/
|
||||
public function getTarget(): string
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This field maps the "presentation" resource property, which is non-standard.
|
||||
* Consider using "iframe" and/or "window" instead.
|
||||
*/
|
||||
public function setTarget(string $value): LtiDeepLinkResource
|
||||
{
|
||||
$this->target = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIframe(): ?LtiDeepLinkResourceIframe
|
||||
{
|
||||
return $this->iframe;
|
||||
}
|
||||
|
||||
public function setIframe(?LtiDeepLinkResourceIframe $iframe): LtiDeepLinkResource
|
||||
{
|
||||
$this->iframe = $iframe;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWindow(): ?LtiDeepLinkResourceWindow
|
||||
{
|
||||
return $this->window;
|
||||
}
|
||||
|
||||
public function setWindow(?LtiDeepLinkResourceWindow $window): LtiDeepLinkResource
|
||||
{
|
||||
$this->window = $window;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAvailabilityInterval(): ?LtiDeepLinkDateTimeInterval
|
||||
{
|
||||
return $this->availability_interval;
|
||||
}
|
||||
|
||||
public function setAvailabilityInterval(?LtiDeepLinkDateTimeInterval $availabilityInterval): LtiDeepLinkResource
|
||||
{
|
||||
$this->availability_interval = $availabilityInterval;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSubmissionInterval(): ?LtiDeepLinkDateTimeInterval
|
||||
{
|
||||
return $this->submission_interval;
|
||||
}
|
||||
|
||||
public function setSubmissionInterval(?LtiDeepLinkDateTimeInterval $submissionInterval): LtiDeepLinkResource
|
||||
{
|
||||
$this->submission_interval = $submissionInterval;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$resource = [
|
||||
'type' => $this->type,
|
||||
];
|
||||
|
||||
if (isset($this->title)) {
|
||||
$resource['title'] = $this->title;
|
||||
}
|
||||
if (isset($this->text)) {
|
||||
$resource['text'] = $this->text;
|
||||
}
|
||||
if (isset($this->url)) {
|
||||
$resource['url'] = $this->url;
|
||||
}
|
||||
if (!empty($this->custom_params)) {
|
||||
$resource['custom'] = $this->custom_params;
|
||||
}
|
||||
if (isset($this->icon)) {
|
||||
$resource['icon'] = $this->icon->toArray();
|
||||
}
|
||||
if (isset($this->thumbnail)) {
|
||||
$resource['thumbnail'] = $this->thumbnail->toArray();
|
||||
}
|
||||
if ($this->line_item !== null) {
|
||||
$resource['lineItem'] = [
|
||||
'scoreMaximum' => $this->line_item->getScoreMaximum(),
|
||||
'label' => $this->line_item->getLabel(),
|
||||
];
|
||||
}
|
||||
|
||||
// Kept for backwards compatibility
|
||||
if (!isset($this->iframe) && !isset($this->window)) {
|
||||
$resource['presentation'] = [
|
||||
'documentTarget' => $this->target,
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($this->iframe)) {
|
||||
$resource['iframe'] = $this->iframe->toArray();
|
||||
}
|
||||
if (isset($this->window)) {
|
||||
$resource['window'] = $this->window->toArray();
|
||||
}
|
||||
if (isset($this->availability_interval)) {
|
||||
$resource['available'] = $this->availability_interval->toArray();
|
||||
}
|
||||
if (isset($this->submission_interval)) {
|
||||
$resource['submission'] = $this->submission_interval->toArray();
|
||||
}
|
||||
|
||||
return $resource;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3;
|
||||
|
||||
class LtiDeepLinkResourceIcon
|
||||
{
|
||||
private $url;
|
||||
private $width;
|
||||
private $height;
|
||||
|
||||
public function __construct(string $url, int $width, int $height)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->width = $width;
|
||||
$this->height = $height;
|
||||
}
|
||||
|
||||
public static function new(string $url, int $width, int $height): LtiDeepLinkResourceIcon
|
||||
{
|
||||
return new LtiDeepLinkResourceIcon($url, $width, $height);
|
||||
}
|
||||
|
||||
public function setUrl(string $url): LtiDeepLinkResourceIcon
|
||||
{
|
||||
$this->url = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUrl(): string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
public function setWidth(int $width): LtiDeepLinkResourceIcon
|
||||
{
|
||||
$this->width = $width;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWidth(): int
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
public function setHeight(int $height): LtiDeepLinkResourceIcon
|
||||
{
|
||||
$this->height = $height;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHeight(): int
|
||||
{
|
||||
return $this->height;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'url' => $this->url,
|
||||
'width' => $this->width,
|
||||
'height' => $this->height,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3;
|
||||
|
||||
class LtiDeepLinkResourceIframe
|
||||
{
|
||||
private ?int $width;
|
||||
private ?int $height;
|
||||
|
||||
public function __construct(int $width = null, int $height = null)
|
||||
{
|
||||
$this->width = $width ?? null;
|
||||
$this->height = $height ?? null;
|
||||
}
|
||||
|
||||
public static function new(): LtiDeepLinkResourceIframe
|
||||
{
|
||||
return new LtiDeepLinkResourceIframe();
|
||||
}
|
||||
|
||||
public function setWidth(?int $width): LtiDeepLinkResourceIframe
|
||||
{
|
||||
$this->width = $width;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWidth(): ?int
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
public function setHeight(?int $height): LtiDeepLinkResourceIframe
|
||||
{
|
||||
$this->height = $height;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHeight(): ?int
|
||||
{
|
||||
return $this->height;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$iframe = [];
|
||||
|
||||
if (isset($this->width)) {
|
||||
$iframe['width'] = $this->width;
|
||||
}
|
||||
if (isset($this->height)) {
|
||||
$iframe['height'] = $this->height;
|
||||
}
|
||||
|
||||
return $iframe;
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3;
|
||||
|
||||
class LtiDeepLinkResourceWindow
|
||||
{
|
||||
private ?string $target_name;
|
||||
private ?int $width;
|
||||
private ?int $height;
|
||||
private ?string $window_features;
|
||||
|
||||
public function __construct(string $targetName = null, int $width = null, int $height = null, string $windowFeatures = null)
|
||||
{
|
||||
$this->target_name = $targetName ?? null;
|
||||
$this->width = $width ?? null;
|
||||
$this->height = $height ?? null;
|
||||
$this->window_features = $windowFeatures ?? null;
|
||||
}
|
||||
|
||||
public static function new(): LtiDeepLinkResourceWindow
|
||||
{
|
||||
return new LtiDeepLinkResourceWindow();
|
||||
}
|
||||
|
||||
public function setTargetName(?string $targetName): LtiDeepLinkResourceWindow
|
||||
{
|
||||
$this->target_name = $targetName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTargetName(): ?string
|
||||
{
|
||||
return $this->target_name;
|
||||
}
|
||||
|
||||
public function setWidth(?int $width): LtiDeepLinkResourceWindow
|
||||
{
|
||||
$this->width = $width;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWidth(): ?int
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
public function setHeight(?int $height): LtiDeepLinkResourceWindow
|
||||
{
|
||||
$this->height = $height;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHeight(): ?int
|
||||
{
|
||||
return $this->height;
|
||||
}
|
||||
|
||||
public function setWindowFeatures(?string $windowFeatures): LtiDeepLinkResourceWindow
|
||||
{
|
||||
$this->window_features = $windowFeatures;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWindowFeatures(): ?string
|
||||
{
|
||||
return $this->window_features;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$window = [];
|
||||
|
||||
if (isset($this->target_name)) {
|
||||
$window['targetName'] = $this->target_name;
|
||||
}
|
||||
if (isset($this->width)) {
|
||||
$window['width'] = $this->width;
|
||||
}
|
||||
if (isset($this->height)) {
|
||||
$window['height'] = $this->height;
|
||||
}
|
||||
if (isset($this->window_features)) {
|
||||
$window['windowFeatures'] = $this->window_features;
|
||||
}
|
||||
|
||||
return $window;
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,18 @@
|
||||
|
||||
namespace Packback\Lti1p3;
|
||||
|
||||
class LtiDeployment
|
||||
{
|
||||
private $deployment_id;
|
||||
use Packback\Lti1p3\Interfaces\ILtiDeployment;
|
||||
|
||||
public static function new()
|
||||
class LtiDeployment implements ILtiDeployment
|
||||
{
|
||||
public function __construct(
|
||||
private $deployment_id
|
||||
) {
|
||||
}
|
||||
|
||||
public static function new($deployment_id): self
|
||||
{
|
||||
return new LtiDeployment();
|
||||
return new LtiDeployment($deployment_id);
|
||||
}
|
||||
|
||||
public function getDeploymentId()
|
||||
@@ -16,7 +21,7 @@ class LtiDeployment
|
||||
return $this->deployment_id;
|
||||
}
|
||||
|
||||
public function setDeploymentId($deployment_id)
|
||||
public function setDeploymentId($deployment_id): self
|
||||
{
|
||||
$this->deployment_id = $deployment_id;
|
||||
|
||||
|
||||
+26
-19
@@ -2,8 +2,11 @@
|
||||
|
||||
namespace Packback\Lti1p3;
|
||||
|
||||
use Packback\Lti1p3\Concerns\JsonStringable;
|
||||
|
||||
class LtiGrade
|
||||
{
|
||||
use JsonStringable;
|
||||
private $score_given;
|
||||
private $score_maximum;
|
||||
private $comment;
|
||||
@@ -14,7 +17,7 @@ class LtiGrade
|
||||
private $submission_review;
|
||||
private $canvas_extension;
|
||||
|
||||
public function __construct(array $grade = null)
|
||||
public function __construct(?array $grade = null)
|
||||
{
|
||||
$this->score_given = $grade['scoreGiven'] ?? null;
|
||||
$this->score_maximum = $grade['scoreMaximum'] ?? null;
|
||||
@@ -27,10 +30,9 @@ class LtiGrade
|
||||
$this->canvas_extension = $grade['https://canvas.instructure.com/lti/submission'] ?? null;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
public function getArray(): array
|
||||
{
|
||||
// Additionally, includes the call back to filter out only NULL values
|
||||
$request = array_filter([
|
||||
return [
|
||||
'scoreGiven' => $this->score_given,
|
||||
'scoreMaximum' => $this->score_maximum,
|
||||
'comment' => $this->comment,
|
||||
@@ -40,15 +42,13 @@ class LtiGrade
|
||||
'userId' => $this->user_id,
|
||||
'submissionReview' => $this->submission_review,
|
||||
'https://canvas.instructure.com/lti/submission' => $this->canvas_extension,
|
||||
], '\Packback\Lti1p3\Helpers\Helpers::checkIfNullValue');
|
||||
|
||||
return json_encode($request);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to allow for method chaining without having to assign to a variable first.
|
||||
*/
|
||||
public static function new()
|
||||
public static function new(): self
|
||||
{
|
||||
return new LtiGrade();
|
||||
}
|
||||
@@ -58,7 +58,7 @@ class LtiGrade
|
||||
return $this->score_given;
|
||||
}
|
||||
|
||||
public function setScoreGiven($value)
|
||||
public function setScoreGiven($value): self
|
||||
{
|
||||
$this->score_given = $value;
|
||||
|
||||
@@ -70,7 +70,7 @@ class LtiGrade
|
||||
return $this->score_maximum;
|
||||
}
|
||||
|
||||
public function setScoreMaximum($value)
|
||||
public function setScoreMaximum($value): self
|
||||
{
|
||||
$this->score_maximum = $value;
|
||||
|
||||
@@ -82,7 +82,7 @@ class LtiGrade
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
public function setComment($comment)
|
||||
public function setComment($comment): self
|
||||
{
|
||||
$this->comment = $comment;
|
||||
|
||||
@@ -94,7 +94,7 @@ class LtiGrade
|
||||
return $this->activity_progress;
|
||||
}
|
||||
|
||||
public function setActivityProgress($value)
|
||||
public function setActivityProgress($value): self
|
||||
{
|
||||
$this->activity_progress = $value;
|
||||
|
||||
@@ -106,7 +106,7 @@ class LtiGrade
|
||||
return $this->grading_progress;
|
||||
}
|
||||
|
||||
public function setGradingProgress($value)
|
||||
public function setGradingProgress($value): self
|
||||
{
|
||||
$this->grading_progress = $value;
|
||||
|
||||
@@ -118,7 +118,7 @@ class LtiGrade
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
||||
public function setTimestamp($value)
|
||||
public function setTimestamp($value): self
|
||||
{
|
||||
$this->timestamp = $value;
|
||||
|
||||
@@ -130,7 +130,7 @@ class LtiGrade
|
||||
return $this->user_id;
|
||||
}
|
||||
|
||||
public function setUserId($value)
|
||||
public function setUserId($value): self
|
||||
{
|
||||
$this->user_id = $value;
|
||||
|
||||
@@ -142,7 +142,7 @@ class LtiGrade
|
||||
return $this->submission_review;
|
||||
}
|
||||
|
||||
public function setSubmissionReview($value)
|
||||
public function setSubmissionReview($value): self
|
||||
{
|
||||
$this->submission_review = $value;
|
||||
|
||||
@@ -154,9 +154,16 @@ class LtiGrade
|
||||
return $this->canvas_extension;
|
||||
}
|
||||
|
||||
// Custom Extension for Canvas.
|
||||
// https://documentation.instructure.com/doc/api/score.html
|
||||
public function setCanvasExtension($value)
|
||||
/**
|
||||
* Add custom extensions for Canvas.
|
||||
*
|
||||
* Disclaimer: You should only set this if your LMS is Canvas.
|
||||
* Some LMS (e.g. Schoology) include validation logic that will throw if there
|
||||
* is unexpected data. And, the type of LMS cannot simply be inferred by their URL.
|
||||
*
|
||||
* @see https://documentation.instructure.com/doc/api/score.html
|
||||
*/
|
||||
public function setCanvasExtension($value): self
|
||||
{
|
||||
$this->canvas_extension = $value;
|
||||
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
|
||||
namespace Packback\Lti1p3;
|
||||
|
||||
use Packback\Lti1p3\Concerns\JsonStringable;
|
||||
|
||||
class LtiGradeSubmissionReview
|
||||
{
|
||||
use JsonStringable;
|
||||
private $reviewable_status;
|
||||
private $label;
|
||||
private $url;
|
||||
private $custom;
|
||||
|
||||
public function __construct(array $gradeSubmission = null)
|
||||
public function __construct(?array $gradeSubmission = null)
|
||||
{
|
||||
$this->reviewable_status = $gradeSubmission['reviewableStatus'] ?? null;
|
||||
$this->label = $gradeSubmission['label'] ?? null;
|
||||
@@ -17,21 +20,20 @@ class LtiGradeSubmissionReview
|
||||
$this->custom = $gradeSubmission['custom'] ?? null;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
public function getArray(): array
|
||||
{
|
||||
// Additionally, includes the call back to filter out only NULL values
|
||||
return json_encode(array_filter([
|
||||
return [
|
||||
'reviewableStatus' => $this->reviewable_status,
|
||||
'label' => $this->label,
|
||||
'url' => $this->url,
|
||||
'custom' => $this->custom,
|
||||
], '\Packback\Lti1p3\Helpers\Helpers::checkIfNullValue'));
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to allow for method chaining without having to assign to a variable first.
|
||||
*/
|
||||
public static function new()
|
||||
public static function new(): self
|
||||
{
|
||||
return new LtiGradeSubmissionReview();
|
||||
}
|
||||
@@ -41,7 +43,7 @@ class LtiGradeSubmissionReview
|
||||
return $this->reviewable_status;
|
||||
}
|
||||
|
||||
public function setReviewableStatus($value)
|
||||
public function setReviewableStatus($value): self
|
||||
{
|
||||
$this->reviewable_status = $value;
|
||||
|
||||
@@ -53,7 +55,7 @@ class LtiGradeSubmissionReview
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel($value)
|
||||
public function setLabel($value): self
|
||||
{
|
||||
$this->label = $value;
|
||||
|
||||
@@ -65,7 +67,7 @@ class LtiGradeSubmissionReview
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
public function setUrl($url)
|
||||
public function setUrl($url): self
|
||||
{
|
||||
$this->url = $url;
|
||||
|
||||
@@ -77,7 +79,7 @@ class LtiGradeSubmissionReview
|
||||
return $this->custom;
|
||||
}
|
||||
|
||||
public function setCustom($value)
|
||||
public function setCustom($value): self
|
||||
{
|
||||
$this->custom = $value;
|
||||
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
namespace Packback\Lti1p3;
|
||||
|
||||
use Packback\Lti1p3\Concerns\JsonStringable;
|
||||
|
||||
class LtiLineitem
|
||||
{
|
||||
use JsonStringable;
|
||||
private $id;
|
||||
private $score_maximum;
|
||||
private $label;
|
||||
@@ -12,8 +15,9 @@ class LtiLineitem
|
||||
private $tag;
|
||||
private $start_date_time;
|
||||
private $end_date_time;
|
||||
private ?bool $grades_released;
|
||||
|
||||
public function __construct(array $lineitem = null)
|
||||
public function __construct(?array $lineitem = null)
|
||||
{
|
||||
$this->id = $lineitem['id'] ?? null;
|
||||
$this->score_maximum = $lineitem['scoreMaximum'] ?? null;
|
||||
@@ -23,12 +27,20 @@ class LtiLineitem
|
||||
$this->tag = $lineitem['tag'] ?? null;
|
||||
$this->start_date_time = $lineitem['startDateTime'] ?? null;
|
||||
$this->end_date_time = $lineitem['endDateTime'] ?? null;
|
||||
$this->grades_released = $lineitem['gradesReleased'] ?? null;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
/**
|
||||
* Static function to allow for method chaining without having to assign to a variable first.
|
||||
*/
|
||||
public static function new(?array $lineItem = null): self
|
||||
{
|
||||
// Additionally, includes the call back to filter out only NULL values
|
||||
return json_encode(array_filter([
|
||||
return new LtiLineitem($lineItem);
|
||||
}
|
||||
|
||||
public function getArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'scoreMaximum' => $this->score_maximum,
|
||||
'label' => $this->label,
|
||||
@@ -37,15 +49,8 @@ class LtiLineitem
|
||||
'tag' => $this->tag,
|
||||
'startDateTime' => $this->start_date_time,
|
||||
'endDateTime' => $this->end_date_time,
|
||||
], '\Packback\Lti1p3\Helpers\Helpers::checkIfNullValue'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to allow for method chaining without having to assign to a variable first.
|
||||
*/
|
||||
public static function new()
|
||||
{
|
||||
return new LtiLineitem();
|
||||
'gradesReleased' => $this->grades_released,
|
||||
];
|
||||
}
|
||||
|
||||
public function getId()
|
||||
@@ -53,7 +58,7 @@ class LtiLineitem
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId($value)
|
||||
public function setId($value): self
|
||||
{
|
||||
$this->id = $value;
|
||||
|
||||
@@ -65,7 +70,7 @@ class LtiLineitem
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel($value)
|
||||
public function setLabel($value): self
|
||||
{
|
||||
$this->label = $value;
|
||||
|
||||
@@ -77,7 +82,7 @@ class LtiLineitem
|
||||
return $this->score_maximum;
|
||||
}
|
||||
|
||||
public function setScoreMaximum($value)
|
||||
public function setScoreMaximum($value): self
|
||||
{
|
||||
$this->score_maximum = $value;
|
||||
|
||||
@@ -89,7 +94,7 @@ class LtiLineitem
|
||||
return $this->resource_id;
|
||||
}
|
||||
|
||||
public function setResourceId($value)
|
||||
public function setResourceId($value): self
|
||||
{
|
||||
$this->resource_id = $value;
|
||||
|
||||
@@ -101,7 +106,7 @@ class LtiLineitem
|
||||
return $this->resource_link_id;
|
||||
}
|
||||
|
||||
public function setResourceLinkId($value)
|
||||
public function setResourceLinkId($value): self
|
||||
{
|
||||
$this->resource_link_id = $value;
|
||||
|
||||
@@ -113,7 +118,7 @@ class LtiLineitem
|
||||
return $this->tag;
|
||||
}
|
||||
|
||||
public function setTag($value)
|
||||
public function setTag($value): self
|
||||
{
|
||||
$this->tag = $value;
|
||||
|
||||
@@ -125,7 +130,7 @@ class LtiLineitem
|
||||
return $this->start_date_time;
|
||||
}
|
||||
|
||||
public function setStartDateTime($value)
|
||||
public function setStartDateTime($value): self
|
||||
{
|
||||
$this->start_date_time = $value;
|
||||
|
||||
@@ -137,10 +142,22 @@ class LtiLineitem
|
||||
return $this->end_date_time;
|
||||
}
|
||||
|
||||
public function setEndDateTime($value)
|
||||
public function setEndDateTime($value): self
|
||||
{
|
||||
$this->end_date_time = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGradesReleased(): ?bool
|
||||
{
|
||||
return $this->grades_released;
|
||||
}
|
||||
|
||||
public function setGradesReleased(?bool $value): self
|
||||
{
|
||||
$this->grades_released = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
+171
-124
@@ -6,11 +6,15 @@ use Exception;
|
||||
use Firebase\JWT\ExpiredException;
|
||||
use Firebase\JWT\JWK;
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Packback\Lti1p3\Interfaces\ICache;
|
||||
use Packback\Lti1p3\Interfaces\ICookie;
|
||||
use Packback\Lti1p3\Interfaces\IDatabase;
|
||||
use Packback\Lti1p3\Interfaces\ILtiDeployment;
|
||||
use Packback\Lti1p3\Interfaces\ILtiRegistration;
|
||||
use Packback\Lti1p3\Interfaces\ILtiServiceConnector;
|
||||
use Packback\Lti1p3\Interfaces\IMigrationDatabase;
|
||||
use Packback\Lti1p3\MessageValidators\DeepLinkMessageValidator;
|
||||
use Packback\Lti1p3\MessageValidators\ResourceMessageValidator;
|
||||
use Packback\Lti1p3\MessageValidators\SubmissionReviewMessageValidator;
|
||||
@@ -45,14 +49,13 @@ class LtiMessageLaunch
|
||||
public const ERR_INVALID_MESSAGE = 'Message validation failed.';
|
||||
public const ERR_INVALID_ALG = 'Invalid alg was specified in the JWT header.';
|
||||
public const ERR_MISMATCHED_ALG_KEY = 'The alg specified in the JWT header is incompatible with the JWK key type.';
|
||||
private $db;
|
||||
private $cache;
|
||||
private $cookie;
|
||||
private $serviceConnector;
|
||||
private $request;
|
||||
private $jwt;
|
||||
private $registration;
|
||||
private $launch_id;
|
||||
public const ERR_OAUTH_KEY_SIGN_NOT_VERIFIED = 'Unable to upgrade from LTI 1.1 to 1.3. No OAuth Consumer Key matched this signature.';
|
||||
public const ERR_OAUTH_KEY_SIGN_MISSING = 'Unable to upgrade from LTI 1.1 to 1.3. The oauth_consumer_key_sign was not provided.';
|
||||
private array $request;
|
||||
private array $jwt;
|
||||
private ?ILtiRegistration $registration;
|
||||
private ?ILtiDeployment $deployment;
|
||||
public string $launch_id;
|
||||
|
||||
// See https://www.imsglobal.org/spec/security/v1p1#approved-jwt-signing-algorithms.
|
||||
private static $ltiSupportedAlgs = [
|
||||
@@ -64,105 +67,115 @@ class LtiMessageLaunch
|
||||
'ES512' => 'EC',
|
||||
];
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function __construct(
|
||||
IDatabase $database,
|
||||
ICache $cache = null,
|
||||
ICookie $cookie = null,
|
||||
ILtiServiceConnector $serviceConnector = null
|
||||
private IDatabase $db,
|
||||
private ICache $cache,
|
||||
private ICookie $cookie,
|
||||
private ILtiServiceConnector $serviceConnector
|
||||
) {
|
||||
$this->db = $database;
|
||||
|
||||
$this->launch_id = uniqid('lti1p3_launch_', true);
|
||||
|
||||
$this->cache = $cache;
|
||||
$this->cookie = $cookie;
|
||||
$this->serviceConnector = $serviceConnector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to allow for method chaining without having to assign to a variable first.
|
||||
*/
|
||||
public static function new(
|
||||
IDatabase $database,
|
||||
ICache $cache = null,
|
||||
ICookie $cookie = null,
|
||||
ILtiServiceConnector $serviceConnector = null
|
||||
) {
|
||||
return new LtiMessageLaunch($database, $cache, $cookie, $serviceConnector);
|
||||
IDatabase $db,
|
||||
ICache $cache,
|
||||
ICookie $cookie,
|
||||
ILtiServiceConnector $serviceConnector
|
||||
): self {
|
||||
return new LtiMessageLaunch($db, $cache, $cookie, $serviceConnector);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ICache $cache Instance of the Cache interface used to loading and storing launches. If non is provided launch data will be store in $_SESSION.
|
||||
* @return LtiMessageLaunch A populated and validated LtiMessageLaunch
|
||||
*
|
||||
* @throws LtiException Will throw an LtiException if validation fails or launch cannot be found
|
||||
*/
|
||||
public static function fromCache(
|
||||
$launch_id,
|
||||
IDatabase $database,
|
||||
ICache $cache = null,
|
||||
ILtiServiceConnector $serviceConnector = null
|
||||
) {
|
||||
$new = new LtiMessageLaunch($database, $cache, null, $serviceConnector);
|
||||
string $launch_id,
|
||||
IDatabase $db,
|
||||
ICache $cache,
|
||||
ICookie $cookie,
|
||||
ILtiServiceConnector $serviceConnector
|
||||
): self {
|
||||
$new = new LtiMessageLaunch($db, $cache, $cookie, $serviceConnector);
|
||||
$new->launch_id = $launch_id;
|
||||
$new->jwt = ['body' => $new->cache->getLaunchData($launch_id)];
|
||||
|
||||
return $new->validateRegistration();
|
||||
}
|
||||
|
||||
public function setRequest(array $request): self
|
||||
{
|
||||
$this->request = $request;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function initialize(array $request): self
|
||||
{
|
||||
return $this->setRequest($request)
|
||||
->validate()
|
||||
->migrate()
|
||||
->cacheLaunchData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates all aspects of an incoming LTI message launch and caches the launch if successful.
|
||||
*
|
||||
* @param array|string $request An array of post request parameters. If not set will default to $_POST.
|
||||
* @return LtiMessageLaunch Will return $this if validation is successful
|
||||
*
|
||||
* @throws LtiException Will throw an LtiException if validation fails
|
||||
*/
|
||||
public function validate(array $request = null)
|
||||
public function validate(): self
|
||||
{
|
||||
if ($request === null) {
|
||||
$request = $_POST;
|
||||
}
|
||||
$this->request = $request;
|
||||
|
||||
return $this->validateState()
|
||||
->validateJwtFormat()
|
||||
->validateNonce()
|
||||
->validateRegistration()
|
||||
->validateJwtSignature()
|
||||
->validateDeployment()
|
||||
->validateMessage()
|
||||
->cacheLaunchData();
|
||||
->validateMessage();
|
||||
}
|
||||
|
||||
public function migrate(): self
|
||||
{
|
||||
if (!$this->shouldMigrate()) {
|
||||
return $this->ensureDeploymentExists();
|
||||
}
|
||||
|
||||
if (!isset($this->jwt['body'][LtiConstants::LTI1P1]['oauth_consumer_key_sign'])) {
|
||||
throw new LtiException(static::ERR_OAUTH_KEY_SIGN_MISSING);
|
||||
}
|
||||
|
||||
if (!$this->matchingLti1p1KeyExists()) {
|
||||
throw new LtiException(static::ERR_OAUTH_KEY_SIGN_NOT_VERIFIED);
|
||||
}
|
||||
|
||||
$this->deployment = $this->db->migrateFromLti1p1($this);
|
||||
|
||||
return $this->ensureDeploymentExists();
|
||||
}
|
||||
|
||||
public function cacheLaunchData(): self
|
||||
{
|
||||
$this->cache->cacheLaunchData($this->launch_id, $this->jwt['body']);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function hasNrps()
|
||||
public function hasNrps(): bool
|
||||
{
|
||||
return !empty($this->jwt['body'][LtiConstants::NRPS_CLAIM_SERVICE]['context_memberships_url']);
|
||||
return isset($this->jwt['body'][LtiConstants::NRPS_CLAIM_SERVICE]['context_memberships_url']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function getNrps()
|
||||
public function getNrps(): LtiNamesRolesProvisioningService
|
||||
{
|
||||
return new LtiNamesRolesProvisioningService(
|
||||
$this->serviceConnector,
|
||||
@@ -173,20 +186,16 @@ class LtiMessageLaunch
|
||||
|
||||
/**
|
||||
* Returns whether or not the current launch can use the groups service.
|
||||
*
|
||||
* @return bool Returns a boolean indicating the availability of groups
|
||||
*/
|
||||
public function hasGs()
|
||||
public function hasGs(): bool
|
||||
{
|
||||
return !empty($this->jwt['body'][LtiConstants::GS_CLAIM_SERVICE]['context_groups_url']);
|
||||
return isset($this->jwt['body'][LtiConstants::GS_CLAIM_SERVICE]['context_groups_url']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function getGs()
|
||||
public function getGs(): LtiCourseGroupsService
|
||||
{
|
||||
return new LtiCourseGroupsService(
|
||||
$this->serviceConnector,
|
||||
@@ -197,20 +206,16 @@ class LtiMessageLaunch
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function hasAgs()
|
||||
public function hasAgs(): bool
|
||||
{
|
||||
return !empty($this->jwt['body'][LtiConstants::AGS_CLAIM_ENDPOINT]);
|
||||
return isset($this->jwt['body'][LtiConstants::AGS_CLAIM_ENDPOINT]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function getAgs()
|
||||
public function getAgs(): LtiAssignmentsGradesService
|
||||
{
|
||||
return new LtiAssignmentsGradesService(
|
||||
$this->serviceConnector,
|
||||
@@ -221,20 +226,16 @@ class LtiMessageLaunch
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function isDeepLinkLaunch()
|
||||
public function isDeepLinkLaunch(): bool
|
||||
{
|
||||
return $this->jwt['body'][LtiConstants::MESSAGE_TYPE] === static::TYPE_DEEPLINK;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function getDeepLink()
|
||||
public function getDeepLink(): LtiDeepLink
|
||||
{
|
||||
return new LtiDeepLink(
|
||||
$this->registration,
|
||||
@@ -245,45 +246,37 @@ class LtiMessageLaunch
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function isSubmissionReviewLaunch()
|
||||
public function isSubmissionReviewLaunch(): bool
|
||||
{
|
||||
return $this->jwt['body'][LtiConstants::MESSAGE_TYPE] === static::TYPE_SUBMISSIONREVIEW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the current launch is a resource launch.
|
||||
*
|
||||
* @return bool Returns true if the current launch is a resource launch
|
||||
*/
|
||||
public function isResourceLaunch()
|
||||
public function isResourceLaunch(): bool
|
||||
{
|
||||
return $this->jwt['body'][LtiConstants::MESSAGE_TYPE] === static::TYPE_RESOURCELINK;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function getLaunchData()
|
||||
public function getLaunchData(): array
|
||||
{
|
||||
return $this->jwt['body'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique launch id for the current launch.
|
||||
*
|
||||
* @return string A unique identifier used to re-reference the current launch in subsequent requests
|
||||
*/
|
||||
public function getLaunchId()
|
||||
public function getLaunchId(): string
|
||||
{
|
||||
return $this->launch_id;
|
||||
}
|
||||
|
||||
public static function getMissingRegistrationErrorMsg(string $issuerUrl, string $clientId = null): string
|
||||
public static function getMissingRegistrationErrorMsg(string $issuerUrl, ?string $clientId = null): string
|
||||
{
|
||||
// Guard against client ID being null
|
||||
if (!isset($clientId)) {
|
||||
@@ -296,7 +289,10 @@ class LtiMessageLaunch
|
||||
return str_replace($search, $replace, static::ERR_MISSING_REGISTRATION);
|
||||
}
|
||||
|
||||
private function getPublicKey()
|
||||
/**
|
||||
* @throws LtiException
|
||||
*/
|
||||
private function getPublicKey(): Key
|
||||
{
|
||||
$request = new ServiceRequest(
|
||||
ServiceRequest::METHOD_GET,
|
||||
@@ -358,22 +354,15 @@ class LtiMessageLaunch
|
||||
throw new LtiException(static::ERR_MISMATCHED_ALG_KEY);
|
||||
}
|
||||
|
||||
private function jwtAlgMatchesJwkKty($key): bool
|
||||
private function jwtAlgMatchesJwkKty(array $key): bool
|
||||
{
|
||||
$jwtAlg = $this->jwt['header']['alg'];
|
||||
|
||||
return isset(static::$ltiSupportedAlgs[$jwtAlg]) &&
|
||||
static::$ltiSupportedAlgs[$jwtAlg] === $key['kty'];
|
||||
return isset(self::$ltiSupportedAlgs[$jwtAlg]) &&
|
||||
self::$ltiSupportedAlgs[$jwtAlg] === $key['kty'];
|
||||
}
|
||||
|
||||
private function cacheLaunchData()
|
||||
{
|
||||
$this->cache->cacheLaunchData($this->launch_id, $this->jwt['body']);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function validateState()
|
||||
protected function validateState(): self
|
||||
{
|
||||
// Check State for OIDC.
|
||||
if ($this->cookie->getCookie(LtiOidcLogin::COOKIE_PREFIX.$this->request['state']) !== $this->request['state']) {
|
||||
@@ -384,16 +373,14 @@ class LtiMessageLaunch
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function validateJwtFormat()
|
||||
protected function validateJwtFormat(): self
|
||||
{
|
||||
$jwt = $this->request['id_token'] ?? null;
|
||||
|
||||
if (empty($jwt)) {
|
||||
if (!isset($this->request['id_token'])) {
|
||||
throw new LtiException(static::ERR_MISSING_ID_TOKEN);
|
||||
}
|
||||
|
||||
// Get parts of JWT.
|
||||
$jwt_parts = explode('.', $jwt);
|
||||
$jwt_parts = explode('.', $this->request['id_token']);
|
||||
|
||||
if (count($jwt_parts) !== 3) {
|
||||
// Invalid number of parts in JWT.
|
||||
@@ -408,7 +395,7 @@ class LtiMessageLaunch
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function validateNonce()
|
||||
protected function validateNonce(): self
|
||||
{
|
||||
if (!isset($this->jwt['body']['nonce'])) {
|
||||
throw new LtiException(static::ERR_MISSING_NONCE);
|
||||
@@ -420,14 +407,14 @@ class LtiMessageLaunch
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function validateRegistration()
|
||||
protected function validateRegistration(): self
|
||||
{
|
||||
// Find registration.
|
||||
$clientId = is_array($this->jwt['body']['aud']) ? $this->jwt['body']['aud'][0] : $this->jwt['body']['aud'];
|
||||
$clientId = $this->getAud();
|
||||
$issuerUrl = $this->jwt['body']['iss'];
|
||||
$this->registration = $this->db->findRegistrationByIssuer($issuerUrl, $clientId);
|
||||
|
||||
if (empty($this->registration)) {
|
||||
if (!isset($this->registration)) {
|
||||
throw new LtiException($this->getMissingRegistrationErrorMsg($issuerUrl, $clientId));
|
||||
}
|
||||
|
||||
@@ -440,7 +427,7 @@ class LtiMessageLaunch
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function validateJwtSignature()
|
||||
protected function validateJwtSignature(): self
|
||||
{
|
||||
if (!isset($this->jwt['header']['kid'])) {
|
||||
throw new LtiException(static::ERR_NO_KID);
|
||||
@@ -461,27 +448,26 @@ class LtiMessageLaunch
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function validateDeployment()
|
||||
protected function validateDeployment(): self
|
||||
{
|
||||
if (!isset($this->jwt['body'][LtiConstants::DEPLOYMENT_ID])) {
|
||||
throw new LtiException(static::ERR_MISSING_DEPLOYEMENT_ID);
|
||||
}
|
||||
|
||||
// Find deployment.
|
||||
$client_id = is_array($this->jwt['body']['aud']) ? $this->jwt['body']['aud'][0] : $this->jwt['body']['aud'];
|
||||
$deployment = $this->db->findDeployment($this->jwt['body']['iss'], $this->jwt['body'][LtiConstants::DEPLOYMENT_ID], $client_id);
|
||||
$client_id = $this->getAud();
|
||||
$this->deployment = $this->db->findDeployment($this->jwt['body']['iss'], $this->jwt['body'][LtiConstants::DEPLOYMENT_ID], $client_id);
|
||||
|
||||
if (empty($deployment)) {
|
||||
// deployment not recognized.
|
||||
throw new LtiException(static::ERR_NO_DEPLOYMENT);
|
||||
if (!$this->canMigrate()) {
|
||||
return $this->ensureDeploymentExists();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function validateMessage()
|
||||
protected function validateMessage(): self
|
||||
{
|
||||
if (empty($this->jwt['body'][LtiConstants::MESSAGE_TYPE])) {
|
||||
if (!isset($this->jwt['body'][LtiConstants::MESSAGE_TYPE])) {
|
||||
// Unable to identify message type.
|
||||
throw new LtiException(static::ERR_INVALID_MESSAGE_TYPE);
|
||||
}
|
||||
@@ -513,4 +499,65 @@ class LtiMessageLaunch
|
||||
// There should be 0-1 validators. This will either return the validator, or null if none apply.
|
||||
return array_shift($applicableValidators);
|
||||
}
|
||||
|
||||
private function getAud(): string
|
||||
{
|
||||
if (is_array($this->jwt['body']['aud'])) {
|
||||
return $this->jwt['body']['aud'][0];
|
||||
} else {
|
||||
return $this->jwt['body']['aud'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LtiException
|
||||
*/
|
||||
private function ensureDeploymentExists(): self
|
||||
{
|
||||
if (!isset($this->deployment)) {
|
||||
throw new LtiException(static::ERR_NO_DEPLOYMENT);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function canMigrate(): bool
|
||||
{
|
||||
return $this->db instanceof IMigrationDatabase;
|
||||
}
|
||||
|
||||
private function shouldMigrate(): bool
|
||||
{
|
||||
return $this->canMigrate()
|
||||
&& $this->db->shouldMigrate($this);
|
||||
}
|
||||
|
||||
private function matchingLti1p1KeyExists(): bool
|
||||
{
|
||||
$keys = $this->db->findLti1p1Keys($this);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if ($this->oauthConsumerKeySignMatches($key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function oauthConsumerKeySignMatches(Lti1p1Key $key): bool
|
||||
{
|
||||
return $this->jwt['body'][LtiConstants::LTI1P1]['oauth_consumer_key_sign'] === $this->getOauthSignature($key);
|
||||
}
|
||||
|
||||
private function getOauthSignature(Lti1p1Key $key): string
|
||||
{
|
||||
return $key->sign(
|
||||
$this->jwt['body'][LtiConstants::DEPLOYMENT_ID],
|
||||
$this->jwt['body']['iss'],
|
||||
$this->getAud(),
|
||||
$this->jwt['body']['exp'],
|
||||
$this->jwt['body']['nonce']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace Packback\Lti1p3;
|
||||
|
||||
use Packback\Lti1p3\Helpers\Helpers;
|
||||
|
||||
class LtiNamesRolesProvisioningService extends LtiAbstractService
|
||||
{
|
||||
public const CONTENTTYPE_MEMBERSHIPCONTAINER = 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json';
|
||||
@@ -11,11 +13,16 @@ class LtiNamesRolesProvisioningService extends LtiAbstractService
|
||||
return [LtiConstants::NRPS_SCOPE_MEMBERSHIP_READONLY];
|
||||
}
|
||||
|
||||
public function getMembers(): array
|
||||
/**
|
||||
* @param array $options An array of options that can be passed with the context_membership_url such as rlid, since, etc.
|
||||
*/
|
||||
public function getMembers(array $options = []): array
|
||||
{
|
||||
$url = Helpers::buildUrlWithQueryParams($this->getServiceData()['context_memberships_url'], $options);
|
||||
|
||||
$request = new ServiceRequest(
|
||||
ServiceRequest::METHOD_GET,
|
||||
$this->getServiceData()['context_memberships_url'],
|
||||
$url,
|
||||
ServiceRequest::TYPE_GET_MEMBERSHIPS
|
||||
);
|
||||
$request->setAccept(static::CONTENTTYPE_MEMBERSHIPCONTAINER);
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
namespace Packback\Lti1p3;
|
||||
|
||||
use Packback\Lti1p3\Helpers\Helpers;
|
||||
use Packback\Lti1p3\Interfaces\ICache;
|
||||
use Packback\Lti1p3\Interfaces\ICookie;
|
||||
use Packback\Lti1p3\Interfaces\IDatabase;
|
||||
use Packback\Lti1p3\Interfaces\ILtiRegistration;
|
||||
|
||||
class LtiOidcLogin
|
||||
{
|
||||
@@ -12,115 +14,86 @@ class LtiOidcLogin
|
||||
public const ERROR_MSG_LAUNCH_URL = 'No launch URL configured';
|
||||
public const ERROR_MSG_ISSUER = 'Could not find issuer';
|
||||
public const ERROR_MSG_LOGIN_HINT = 'Could not find login hint';
|
||||
private $db;
|
||||
private $cache;
|
||||
private $cookie;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function __construct(IDatabase $database, ICache $cache = null, ICookie $cookie = null)
|
||||
{
|
||||
$this->db = $database;
|
||||
$this->cache = $cache;
|
||||
$this->cookie = $cookie;
|
||||
public function __construct(
|
||||
public IDatabase $db,
|
||||
public ICache $cache,
|
||||
public ICookie $cookie
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to allow for method chaining without having to assign to a variable first.
|
||||
*/
|
||||
public static function new(IDatabase $database, ICache $cache = null, ICookie $cookie = null)
|
||||
public static function new(IDatabase $db, ICache $cache, ICookie $cookie): self
|
||||
{
|
||||
return new LtiOidcLogin($database, $cache, $cookie);
|
||||
return new LtiOidcLogin($db, $cache, $cookie);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the redirect location to return to based on an OIDC third party initiated login request.
|
||||
*
|
||||
* @param string $launch_url URL to redirect back to after the OIDC login. This URL must match exactly a URL white listed in the platform.
|
||||
* @param array|string $request An array of request parameters. If not set will default to $_REQUEST.
|
||||
* @return Redirect returns a redirect object containing the fully formed OIDC login URL
|
||||
*/
|
||||
public function doOidcLoginRedirect($launch_url, array $request = null)
|
||||
public function getRedirectUrl(string $launchUrl, array $request): string
|
||||
{
|
||||
if ($request === null) {
|
||||
$request = $_REQUEST;
|
||||
}
|
||||
|
||||
if (empty($launch_url)) {
|
||||
throw new OidcException(static::ERROR_MSG_LAUNCH_URL, 1);
|
||||
}
|
||||
|
||||
// Validate Request Data.
|
||||
// Validate request data.
|
||||
$registration = $this->validateOidcLogin($request);
|
||||
|
||||
/*
|
||||
* Build OIDC Auth Response.
|
||||
*/
|
||||
// Build OIDC Auth response.
|
||||
$authParams = $this->getAuthParams($launchUrl, $registration->getClientId(), $request);
|
||||
|
||||
// Generate State.
|
||||
return Helpers::buildUrlWithQueryParams($registration->getAuthLoginUrl(), $authParams);
|
||||
}
|
||||
|
||||
public function validateOidcLogin(array $request): ILtiRegistration
|
||||
{
|
||||
if (!isset($request['iss'])) {
|
||||
throw new OidcException(static::ERROR_MSG_ISSUER);
|
||||
}
|
||||
|
||||
if (!isset($request['login_hint'])) {
|
||||
throw new OidcException(static::ERROR_MSG_LOGIN_HINT);
|
||||
}
|
||||
|
||||
// Fetch registration
|
||||
$clientId = $request['client_id'] ?? null;
|
||||
$registration = $this->db->findRegistrationByIssuer($request['iss'], $clientId);
|
||||
|
||||
if (!isset($registration)) {
|
||||
$errorMsg = LtiMessageLaunch::getMissingRegistrationErrorMsg($request['iss'], $clientId);
|
||||
|
||||
throw new OidcException($errorMsg);
|
||||
}
|
||||
|
||||
return $registration;
|
||||
}
|
||||
|
||||
public function getAuthParams(string $launchUrl, string $clientId, array $request): array
|
||||
{
|
||||
// Set cookie (short lived)
|
||||
$state = static::secureRandomString('state-');
|
||||
$this->cookie->setCookie(static::COOKIE_PREFIX.$state, $state, 60);
|
||||
|
||||
// Generate Nonce.
|
||||
$nonce = static::secureRandomString('nonce-');
|
||||
$this->cache->cacheNonce($nonce, $state);
|
||||
|
||||
// Build Response.
|
||||
$auth_params = [
|
||||
$authParams = [
|
||||
'scope' => 'openid', // OIDC Scope.
|
||||
'response_type' => 'id_token', // OIDC response is always an id token.
|
||||
'response_mode' => 'form_post', // OIDC response is always a form post.
|
||||
'prompt' => 'none', // Don't prompt user on redirect.
|
||||
'client_id' => $registration->getClientId(), // Registered client id.
|
||||
'redirect_uri' => $launch_url, // URL to return to after login.
|
||||
'client_id' => $clientId, // Registered client id.
|
||||
'redirect_uri' => $launchUrl, // URL to return to after login.
|
||||
'state' => $state, // State to identify browser session.
|
||||
'nonce' => $nonce, // Prevent replay attacks.
|
||||
'login_hint' => $request['login_hint'], // Login hint to identify platform session.
|
||||
];
|
||||
|
||||
// Pass back LTI message hint if we have it.
|
||||
if (isset($request['lti_message_hint'])) {
|
||||
// LTI message hint to identify LTI context within the platform.
|
||||
$auth_params['lti_message_hint'] = $request['lti_message_hint'];
|
||||
$authParams['lti_message_hint'] = $request['lti_message_hint'];
|
||||
}
|
||||
|
||||
$auth_login_return_url = $registration->getAuthLoginUrl().'?'.http_build_query($auth_params, '', '&');
|
||||
|
||||
// Return auth redirect.
|
||||
return new Redirect($auth_login_return_url, http_build_query($request, '', '&'));
|
||||
}
|
||||
|
||||
public function validateOidcLogin($request)
|
||||
{
|
||||
// Validate Issuer.
|
||||
if (empty($request['iss'])) {
|
||||
throw new OidcException(static::ERROR_MSG_ISSUER, 1);
|
||||
}
|
||||
|
||||
// Validate Login Hint.
|
||||
if (empty($request['login_hint'])) {
|
||||
throw new OidcException(static::ERROR_MSG_LOGIN_HINT, 1);
|
||||
}
|
||||
|
||||
// Fetch Registration Details.
|
||||
$clientId = $request['client_id'] ?? null;
|
||||
$registration = $this->db->findRegistrationByIssuer($request['iss'], $clientId);
|
||||
|
||||
// Check we got something.
|
||||
if (empty($registration)) {
|
||||
$errorMsg = LtiMessageLaunch::getMissingRegistrationErrorMsg($request['iss'], $clientId);
|
||||
|
||||
throw new OidcException($errorMsg, 1);
|
||||
}
|
||||
|
||||
// Return Registration.
|
||||
return $registration;
|
||||
return $authParams;
|
||||
}
|
||||
|
||||
public static function secureRandomString(string $prefix = ''): string
|
||||
|
||||
@@ -6,16 +6,16 @@ use Packback\Lti1p3\Interfaces\ILtiRegistration;
|
||||
|
||||
class LtiRegistration implements ILtiRegistration
|
||||
{
|
||||
private $issuer;
|
||||
private $clientId;
|
||||
private $keySetUrl;
|
||||
private $authTokenUrl;
|
||||
private $authLoginUrl;
|
||||
private $authServer;
|
||||
private $toolPrivateKey;
|
||||
private $kid;
|
||||
private ?string $issuer;
|
||||
private ?string $clientId;
|
||||
private ?string $keySetUrl;
|
||||
private ?string $authTokenUrl;
|
||||
private ?string $authLoginUrl;
|
||||
private ?string $authServer;
|
||||
private ?string $toolPrivateKey;
|
||||
private ?string $kid;
|
||||
|
||||
public function __construct(array $registration = [])
|
||||
public function __construct(?array $registration = null)
|
||||
{
|
||||
$this->issuer = $registration['issuer'] ?? null;
|
||||
$this->clientId = $registration['clientId'] ?? null;
|
||||
@@ -27,7 +27,7 @@ class LtiRegistration implements ILtiRegistration
|
||||
$this->kid = $registration['kid'] ?? null;
|
||||
}
|
||||
|
||||
public static function new(array $registration = [])
|
||||
public static function new(?array $registration = null): self
|
||||
{
|
||||
return new LtiRegistration($registration);
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class LtiRegistration implements ILtiRegistration
|
||||
return $this->issuer;
|
||||
}
|
||||
|
||||
public function setIssuer($issuer)
|
||||
public function setIssuer(string $issuer): self
|
||||
{
|
||||
$this->issuer = $issuer;
|
||||
|
||||
@@ -49,55 +49,55 @@ class LtiRegistration implements ILtiRegistration
|
||||
return $this->clientId;
|
||||
}
|
||||
|
||||
public function setClientId($clientId)
|
||||
public function setClientId(string $clientId): self
|
||||
{
|
||||
$this->clientId = $clientId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getKeySetUrl()
|
||||
public function getKeySetUrl(): ?string
|
||||
{
|
||||
return $this->keySetUrl;
|
||||
}
|
||||
|
||||
public function setKeySetUrl($keySetUrl)
|
||||
public function setKeySetUrl(?string $keySetUrl): self
|
||||
{
|
||||
$this->keySetUrl = $keySetUrl;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAuthTokenUrl()
|
||||
public function getAuthTokenUrl(): ?string
|
||||
{
|
||||
return $this->authTokenUrl;
|
||||
}
|
||||
|
||||
public function setAuthTokenUrl($authTokenUrl)
|
||||
public function setAuthTokenUrl(?string $authTokenUrl): self
|
||||
{
|
||||
$this->authTokenUrl = $authTokenUrl;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAuthLoginUrl()
|
||||
public function getAuthLoginUrl(): ?string
|
||||
{
|
||||
return $this->authLoginUrl;
|
||||
}
|
||||
|
||||
public function setAuthLoginUrl($authLoginUrl)
|
||||
public function setAuthLoginUrl(?string $authLoginUrl): self
|
||||
{
|
||||
$this->authLoginUrl = $authLoginUrl;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAuthServer()
|
||||
public function getAuthServer(): ?string
|
||||
{
|
||||
return empty($this->authServer) ? $this->authTokenUrl : $this->authServer;
|
||||
return $this->authServer ?? $this->authTokenUrl;
|
||||
}
|
||||
|
||||
public function setAuthServer($authServer)
|
||||
public function setAuthServer(?string $authServer): self
|
||||
{
|
||||
$this->authServer = $authServer;
|
||||
|
||||
@@ -109,7 +109,7 @@ class LtiRegistration implements ILtiRegistration
|
||||
return $this->toolPrivateKey;
|
||||
}
|
||||
|
||||
public function setToolPrivateKey($toolPrivateKey)
|
||||
public function setToolPrivateKey(string $toolPrivateKey): self
|
||||
{
|
||||
$this->toolPrivateKey = $toolPrivateKey;
|
||||
|
||||
@@ -121,7 +121,7 @@ class LtiRegistration implements ILtiRegistration
|
||||
return $this->kid ?? hash('sha256', trim($this->issuer.$this->clientId));
|
||||
}
|
||||
|
||||
public function setKid($kid)
|
||||
public function setKid(string $kid): self
|
||||
{
|
||||
$this->kid = $kid;
|
||||
|
||||
|
||||
@@ -6,39 +6,38 @@ 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\ILtiRegistration;
|
||||
use Packback\Lti1p3\Interfaces\ILtiServiceConnector;
|
||||
use Packback\Lti1p3\Interfaces\IServiceRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class LtiServiceConnector implements ILtiServiceConnector
|
||||
{
|
||||
public const NEXT_PAGE_REGEX = '/<([^>]*)>; ?rel="next"/i';
|
||||
private $cache;
|
||||
private $client;
|
||||
private $debuggingMode = false;
|
||||
private bool $debuggingMode = false;
|
||||
|
||||
public function __construct(
|
||||
ICache $cache,
|
||||
Client $client
|
||||
private ICache $cache,
|
||||
private Client $client
|
||||
) {
|
||||
$this->cache = $cache;
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
public function setDebuggingMode(bool $enable): void
|
||||
public function setDebuggingMode(bool $enable): self
|
||||
{
|
||||
$this->debuggingMode = $enable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAccessToken(ILtiRegistration $registration, array $scopes)
|
||||
public function getAccessToken(ILtiRegistration $registration, array $scopes): string
|
||||
{
|
||||
// Get a unique cache key for the access token
|
||||
$accessTokenKey = $this->getAccessTokenCacheKey($registration, $scopes);
|
||||
// Get access token from cache if it exists
|
||||
$accessToken = $this->cache->getAccessToken($accessTokenKey);
|
||||
if ($accessToken) {
|
||||
|
||||
if (isset($accessToken)) {
|
||||
return $accessToken;
|
||||
}
|
||||
|
||||
@@ -70,7 +69,8 @@ class LtiServiceConnector implements ILtiServiceConnector
|
||||
$registration->getAuthTokenUrl(),
|
||||
ServiceRequest::TYPE_AUTH
|
||||
);
|
||||
$request->setPayload(['form_params' => $authRequest]);
|
||||
$request->setPayload(['form_params' => $authRequest])
|
||||
->setMaskResponseLogs(true);
|
||||
$response = $this->makeRequest($request);
|
||||
|
||||
$tokenData = $this->getResponseBody($response);
|
||||
@@ -81,7 +81,7 @@ class LtiServiceConnector implements ILtiServiceConnector
|
||||
return $tokenData['access_token'];
|
||||
}
|
||||
|
||||
public function makeRequest(IServiceRequest $request)
|
||||
public function makeRequest(IServiceRequest $request): ResponseInterface
|
||||
{
|
||||
$response = $this->client->request(
|
||||
$request->getMethod(),
|
||||
@@ -100,7 +100,7 @@ class LtiServiceConnector implements ILtiServiceConnector
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getResponseHeaders(Response $response): ?array
|
||||
public function getResponseHeaders(ResponseInterface $response): ?array
|
||||
{
|
||||
$responseHeaders = $response->getHeaders();
|
||||
array_walk($responseHeaders, function (&$value) {
|
||||
@@ -110,7 +110,7 @@ class LtiServiceConnector implements ILtiServiceConnector
|
||||
return $responseHeaders;
|
||||
}
|
||||
|
||||
public function getResponseBody(Response $response): ?array
|
||||
public function getResponseBody(ResponseInterface $response): ?array
|
||||
{
|
||||
$responseBody = (string) $response->getBody();
|
||||
|
||||
@@ -153,7 +153,7 @@ class LtiServiceConnector implements ILtiServiceConnector
|
||||
ILtiRegistration $registration,
|
||||
array $scopes,
|
||||
IServiceRequest $request,
|
||||
string $key = null
|
||||
?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.');
|
||||
@@ -163,46 +163,68 @@ class LtiServiceConnector implements ILtiServiceConnector
|
||||
$nextUrl = $request->getUrl();
|
||||
|
||||
while ($nextUrl) {
|
||||
$request->setUrl($nextUrl);
|
||||
$response = $this->makeServiceRequest($registration, $scopes, $request);
|
||||
|
||||
$page_results = $key === null ? ($response['body'] ?? []) : ($response['body'][$key] ?? []);
|
||||
$results = array_merge($results, $page_results);
|
||||
|
||||
$pageResults = $this->getResultsFromResponse($response, $key);
|
||||
$results = array_merge($results, $pageResults);
|
||||
$nextUrl = $this->getNextUrl($response['headers']);
|
||||
if ($nextUrl) {
|
||||
$request->setUrl($nextUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public static function getLogMessage(
|
||||
IServiceRequest $request,
|
||||
array $responseHeaders,
|
||||
?array $responseBody
|
||||
): string {
|
||||
if ($request->getMaskResponseLogs()) {
|
||||
$responseHeaders = self::maskValues($responseHeaders);
|
||||
$responseBody = self::maskValues($responseBody);
|
||||
}
|
||||
|
||||
$contextArray = [
|
||||
'request_method' => $request->getMethod(),
|
||||
'request_url' => $request->getUrl(),
|
||||
'response_headers' => $responseHeaders,
|
||||
'response_body' => $responseBody,
|
||||
];
|
||||
|
||||
$requestBody = $request->getPayload()['body'] ?? null;
|
||||
|
||||
if (isset($requestBody)) {
|
||||
$contextArray['request_body'] = $requestBody;
|
||||
}
|
||||
|
||||
return implode(' ', array_filter([
|
||||
$request->getErrorPrefix(),
|
||||
json_decode($requestBody)->userId ?? null,
|
||||
json_encode($contextArray),
|
||||
]));
|
||||
}
|
||||
|
||||
private function logRequest(
|
||||
IServiceRequest $request,
|
||||
array $responseHeaders,
|
||||
?array $responseBody
|
||||
): void {
|
||||
$contextArray = [
|
||||
'request_method' => $request->getMethod(),
|
||||
'request_url' => $request->getUrl(),
|
||||
'response_headers' => $responseHeaders,
|
||||
'response_body' => json_encode($responseBody),
|
||||
];
|
||||
|
||||
$requestBody = $request->getPayload()['body'] ?? null;
|
||||
|
||||
if (!empty($requestBody)) {
|
||||
$contextArray['request_body'] = $requestBody;
|
||||
}
|
||||
|
||||
error_log(implode(' ', array_filter([
|
||||
$request->getErrorPrefix(),
|
||||
json_decode($requestBody)->userId ?? null,
|
||||
print_r($contextArray, true),
|
||||
])));
|
||||
error_log(self::getLogMessage($request, $responseHeaders, $responseBody));
|
||||
}
|
||||
|
||||
private function getAccessTokenCacheKey(ILtiRegistration $registration, array $scopes)
|
||||
private static function maskValues(?array $payload): ?array
|
||||
{
|
||||
if (!isset($payload) || empty($payload)) {
|
||||
return $payload;
|
||||
}
|
||||
|
||||
foreach ($payload as $key => $value) {
|
||||
$payload[$key] = '***';
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function getAccessTokenCacheKey(ILtiRegistration $registration, array $scopes): string
|
||||
{
|
||||
sort($scopes);
|
||||
$scopeKey = md5(implode('|', $scopes));
|
||||
@@ -210,7 +232,16 @@ class LtiServiceConnector implements ILtiServiceConnector
|
||||
return $registration->getIssuer().$registration->getClientId().$scopeKey;
|
||||
}
|
||||
|
||||
private function getNextUrl(array $headers)
|
||||
private function getResultsFromResponse(array $response, ?string $key = null): array
|
||||
{
|
||||
if (isset($key)) {
|
||||
return $response['body'][$key] ?? [];
|
||||
}
|
||||
|
||||
return $response['body'] ?? [];
|
||||
}
|
||||
|
||||
private function getNextUrl(array $headers): ?string
|
||||
{
|
||||
$subject = $headers['Link'] ?? $headers['link'] ?? '';
|
||||
preg_match(static::NEXT_PAGE_REGEX, $subject, $matches);
|
||||
|
||||
@@ -17,6 +17,9 @@ abstract class AbstractMessageValidator implements IMessageValidator
|
||||
|
||||
abstract public static function validate(array $jwtBody): void;
|
||||
|
||||
/**
|
||||
* @throws LtiException
|
||||
*/
|
||||
public static function validateGenericMessage(array $jwtBody): void
|
||||
{
|
||||
if (empty($jwtBody['sub'])) {
|
||||
|
||||
@@ -12,6 +12,9 @@ class DeepLinkMessageValidator extends AbstractMessageValidator
|
||||
return LtiConstants::MESSAGE_TYPE_DEEPLINK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LtiException
|
||||
*/
|
||||
public static function validate(array $jwtBody): void
|
||||
{
|
||||
static::validateGenericMessage($jwtBody);
|
||||
|
||||
@@ -12,6 +12,9 @@ class ResourceMessageValidator extends AbstractMessageValidator
|
||||
return LtiConstants::MESSAGE_TYPE_RESOURCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LtiException
|
||||
*/
|
||||
public static function validate(array $jwtBody): void
|
||||
{
|
||||
static::validateGenericMessage($jwtBody);
|
||||
|
||||
@@ -12,6 +12,9 @@ class SubmissionReviewMessageValidator extends AbstractMessageValidator
|
||||
return LtiConstants::MESSAGE_TYPE_SUBMISSIONREVIEW;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LtiException
|
||||
*/
|
||||
public static function validate(array $jwtBody): void
|
||||
{
|
||||
static::validateGenericMessage($jwtBody);
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Packback\Lti1p3;
|
||||
|
||||
use Packback\Lti1p3\Interfaces\ICookie;
|
||||
|
||||
class Redirect
|
||||
{
|
||||
private $location;
|
||||
private $referer_query;
|
||||
private static $CAN_302_COOKIE = 'LTI_302_Redirect';
|
||||
|
||||
public function __construct(string $location, string $referer_query = null)
|
||||
{
|
||||
$this->location = $location;
|
||||
$this->referer_query = $referer_query;
|
||||
}
|
||||
|
||||
public function doRedirect()
|
||||
{
|
||||
header('Location: '.$this->location, true, 302);
|
||||
exit;
|
||||
}
|
||||
|
||||
public function doHybridRedirect(ICookie $cookie)
|
||||
{
|
||||
if (!empty($cookie->getCookie(self::$CAN_302_COOKIE))) {
|
||||
return $this->doRedirect();
|
||||
}
|
||||
$cookie->setCookie(self::$CAN_302_COOKIE, 'true');
|
||||
$this->doJsRedirect();
|
||||
}
|
||||
|
||||
public function getRedirectUrl()
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
public function doJsRedirect()
|
||||
{
|
||||
?>
|
||||
<a id="try-again" target="_blank">If you are not automatically redirected, click here to continue</a>
|
||||
<script>
|
||||
|
||||
document.getElementById('try-again').href=<?php
|
||||
if (empty($this->referer_query)) {
|
||||
echo 'window.location.href';
|
||||
} else {
|
||||
echo "window.location.origin + window.location.pathname + '?".$this->referer_query."'";
|
||||
} ?>;
|
||||
|
||||
var canAccessCookies = function() {
|
||||
if (!navigator.cookieEnabled) {
|
||||
// We don't have access
|
||||
return false;
|
||||
}
|
||||
// Firefox returns true even if we don't actually have access
|
||||
try {
|
||||
if (!document.cookie || document.cookie == "" || document.cookie.indexOf('<?php echo self::$CAN_302_COOKIE; ?>') === -1) {
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (canAccessCookies()) {
|
||||
// We have access, continue with redirect
|
||||
window.location = '<?php echo $this->location; ?>';
|
||||
} else {
|
||||
// We don't have access, reopen flow in a new window.
|
||||
var opened = window.open(document.getElementById('try-again').href, '_blank');
|
||||
if (opened) {
|
||||
document.getElementById('try-again').innerText = "New window opened, click to reopen";
|
||||
} else {
|
||||
document.getElementById('try-again').innerText = "Popup blocked, click to open in a new window";
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -34,20 +34,20 @@ class ServiceRequest implements IServiceRequest
|
||||
|
||||
// NRPS
|
||||
public const TYPE_GET_MEMBERSHIPS = 'get_memberships';
|
||||
private $method;
|
||||
private $url;
|
||||
private $type;
|
||||
private $body;
|
||||
private $payload;
|
||||
private $accessToken;
|
||||
private $contentType = 'application/json';
|
||||
private $accept = 'application/json';
|
||||
|
||||
public function __construct(string $method, string $url, $type = self::UNSUPPORTED)
|
||||
{
|
||||
$this->method = $method;
|
||||
$this->url = $url;
|
||||
$this->type = $type;
|
||||
// Other
|
||||
private $maskResponseLogs = false;
|
||||
|
||||
public function __construct(
|
||||
private string $method,
|
||||
private string $url,
|
||||
private string $type = self::TYPE_UNSUPPORTED
|
||||
) {
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
@@ -120,6 +120,18 @@ class ServiceRequest implements IServiceRequest
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMaskResponseLogs(): bool
|
||||
{
|
||||
return $this->maskResponseLogs;
|
||||
}
|
||||
|
||||
public function setMaskResponseLogs(bool $shouldMask): IServiceRequest
|
||||
{
|
||||
$this->maskResponseLogs = $shouldMask;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getErrorPrefix(): string
|
||||
{
|
||||
$defaultMessage = 'Logging request data:';
|
||||
|
||||
@@ -437,7 +437,7 @@ All rights reserved.</copyright>
|
||||
<location>lti1p3</location>
|
||||
<name>LTI 1.3 Tool Library</name>
|
||||
<description>A library used for building IMS-certified LTI 1.3 tool providers in PHP.</description>
|
||||
<version>5.4.1</version>
|
||||
<version>6.0.0</version>
|
||||
<license>Apache</license>
|
||||
<licenseversion>2.0</licenseversion>
|
||||
<repository>https://github.com/packbackbooks/lti-1-3-php-library</repository>
|
||||
|
||||
Reference in New Issue
Block a user