diff --git a/lib/lti1p3/README.md b/lib/lti1p3/README.md
index 65b09794b70..b7befbbfa11 100644
--- a/lib/lti1p3/README.md
+++ b/lib/lti1p3/README.md
@@ -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.
-
-
diff --git a/lib/lti1p3/src/Concerns/Arrayable.php b/lib/lti1p3/src/Concerns/Arrayable.php
new file mode 100644
index 00000000000..c83f345edc3
--- /dev/null
+++ b/lib/lti1p3/src/Concerns/Arrayable.php
@@ -0,0 +1,15 @@
+getArray());
+ }
+}
diff --git a/lib/lti1p3/src/Concerns/JsonStringable.php b/lib/lti1p3/src/Concerns/JsonStringable.php
new file mode 100644
index 00000000000..29242827c01
--- /dev/null
+++ b/lib/lti1p3/src/Concerns/JsonStringable.php
@@ -0,0 +1,13 @@
+toArray());
+ }
+}
diff --git a/lib/lti1p3/src/DeepLinkResources/DateTimeInterval.php b/lib/lti1p3/src/DeepLinkResources/DateTimeInterval.php
new file mode 100644
index 00000000000..e3b2cef2868
--- /dev/null
+++ b/lib/lti1p3/src/DeepLinkResources/DateTimeInterval.php
@@ -0,0 +1,74 @@
+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);
+ }
+ }
+}
diff --git a/lib/lti1p3/src/DeepLinkResources/HasDimensions.php b/lib/lti1p3/src/DeepLinkResources/HasDimensions.php
new file mode 100644
index 00000000000..ba9431cb0cf
--- /dev/null
+++ b/lib/lti1p3/src/DeepLinkResources/HasDimensions.php
@@ -0,0 +1,30 @@
+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;
+ }
+}
diff --git a/lib/lti1p3/src/DeepLinkResources/Icon.php b/lib/lti1p3/src/DeepLinkResources/Icon.php
new file mode 100644
index 00000000000..267c983fc08
--- /dev/null
+++ b/lib/lti1p3/src/DeepLinkResources/Icon.php
@@ -0,0 +1,43 @@
+ $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;
+ }
+}
diff --git a/lib/lti1p3/src/DeepLinkResources/Iframe.php b/lib/lti1p3/src/DeepLinkResources/Iframe.php
new file mode 100644
index 00000000000..039ea8eb5e7
--- /dev/null
+++ b/lib/lti1p3/src/DeepLinkResources/Iframe.php
@@ -0,0 +1,43 @@
+ $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;
+ }
+}
diff --git a/lib/lti1p3/src/DeepLinkResources/Resource.php b/lib/lti1p3/src/DeepLinkResources/Resource.php
new file mode 100644
index 00000000000..5cbb4b2be9a
--- /dev/null
+++ b/lib/lti1p3/src/DeepLinkResources/Resource.php
@@ -0,0 +1,210 @@
+ $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;
+ }
+}
diff --git a/lib/lti1p3/src/DeepLinkResources/Window.php b/lib/lti1p3/src/DeepLinkResources/Window.php
new file mode 100644
index 00000000000..7ee30ba00ce
--- /dev/null
+++ b/lib/lti1p3/src/DeepLinkResources/Window.php
@@ -0,0 +1,57 @@
+ $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;
+ }
+}
diff --git a/lib/lti1p3/src/Helpers/Helpers.php b/lib/lti1p3/src/Helpers/Helpers.php
index 9c5267ac6c8..ad64d43f778 100644
--- a/lib/lti1p3/src/Helpers/Helpers.php
+++ b/lib/lti1p3/src/Helpers/Helpers.php
@@ -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, '');
}
}
diff --git a/lib/lti1p3/src/ImsStorage/ImsCache.php b/lib/lti1p3/src/ImsStorage/ImsCache.php
deleted file mode 100644
index ed38bb44427..00000000000
--- a/lib/lti1p3/src/ImsStorage/ImsCache.php
+++ /dev/null
@@ -1,79 +0,0 @@
-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));
- }
-}
diff --git a/lib/lti1p3/src/ImsStorage/ImsCookie.php b/lib/lti1p3/src/ImsStorage/ImsCookie.php
deleted file mode 100644
index a98175e2d79..00000000000
--- a/lib/lti1p3/src/ImsStorage/ImsCookie.php
+++ /dev/null
@@ -1,39 +0,0 @@
- 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));
- }
-}
diff --git a/lib/lti1p3/src/Interfaces/ICookie.php b/lib/lti1p3/src/Interfaces/ICookie.php
index 96021c8a1c1..1590acc551a 100644
--- a/lib/lti1p3/src/Interfaces/ICookie.php
+++ b/lib/lti1p3/src/Interfaces/ICookie.php
@@ -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;
}
diff --git a/lib/lti1p3/src/Interfaces/IDatabase.php b/lib/lti1p3/src/Interfaces/IDatabase.php
index d1dd99415a6..d863d0e9422 100644
--- a/lib/lti1p3/src/Interfaces/IDatabase.php
+++ b/lib/lti1p3/src/Interfaces/IDatabase.php
@@ -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;
}
diff --git a/lib/lti1p3/src/Interfaces/ILtiDeployment.php b/lib/lti1p3/src/Interfaces/ILtiDeployment.php
new file mode 100644
index 00000000000..a7092f3ff4b
--- /dev/null
+++ b/lib/lti1p3/src/Interfaces/ILtiDeployment.php
@@ -0,0 +1,11 @@
+
+ */
+ 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;
+}
diff --git a/lib/lti1p3/src/Interfaces/IServiceRequest.php b/lib/lti1p3/src/Interfaces/IServiceRequest.php
index 3987eb05a17..d619b159ef2 100644
--- a/lib/lti1p3/src/Interfaces/IServiceRequest.php
+++ b/lib/lti1p3/src/Interfaces/IServiceRequest.php
@@ -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;
}
diff --git a/lib/lti1p3/src/JwksEndpoint.php b/lib/lti1p3/src/JwksEndpoint.php
index d228c1054eb..312b0241d5b 100644
--- a/lib/lti1p3/src/JwksEndpoint.php
+++ b/lib/lti1p3/src/JwksEndpoint.php
@@ -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());
- }
}
diff --git a/lib/lti1p3/src/Lti1p1Key.php b/lib/lti1p3/src/Lti1p1Key.php
new file mode 100644
index 00000000000..091f9fc51ca
--- /dev/null
+++ b/lib/lti1p3/src/Lti1p1Key.php
@@ -0,0 +1,68 @@
+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);
+ }
+
+}
diff --git a/lib/lti1p3/src/LtiAbstractService.php b/lib/lti1p3/src/LtiAbstractService.php
index 0f33d3adaf1..d0614ba406b 100644
--- a/lib/lti1p3/src/LtiAbstractService.php
+++ b/lib/lti1p3/src/LtiAbstractService.php
@@ -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,
diff --git a/lib/lti1p3/src/LtiAssignmentsGradesService.php b/lib/lti1p3/src/LtiAssignmentsGradesService.php
index 3fffba7af0f..75026e57b18 100644
--- a/lib/lti1p3/src/LtiAssignmentsGradesService.php
+++ b/lib/lti1p3/src/LtiAssignmentsGradesService.php
@@ -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;
+ }
}
diff --git a/lib/lti1p3/src/LtiConstants.php b/lib/lti1p3/src/LtiConstants.php
index 73ffc08cb2f..120ed8d00e9 100644
--- a/lib/lti1p3/src/LtiConstants.php
+++ b/lib/lti1p3/src/LtiConstants.php
@@ -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';
diff --git a/lib/lti1p3/src/LtiCourseGroupsService.php b/lib/lti1p3/src/LtiCourseGroupsService.php
index 4ce8084c0e3..ec60957dbc9 100644
--- a/lib/lti1p3/src/LtiCourseGroupsService.php
+++ b/lib/lti1p3/src/LtiCourseGroupsService.php
@@ -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();
diff --git a/lib/lti1p3/src/LtiDeepLink.php b/lib/lti1p3/src/LtiDeepLink.php
index b42a714e0a5..a1fae707e0e 100644
--- a/lib/lti1p3/src/LtiDeepLink.php
+++ b/lib/lti1p3/src/LtiDeepLink.php
@@ -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;
- }
}
diff --git a/lib/lti1p3/src/LtiDeepLinkDateTimeInterval.php b/lib/lti1p3/src/LtiDeepLinkDateTimeInterval.php
deleted file mode 100644
index b26c6687f69..00000000000
--- a/lib/lti1p3/src/LtiDeepLinkDateTimeInterval.php
+++ /dev/null
@@ -1,72 +0,0 @@
-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;
- }
-}
diff --git a/lib/lti1p3/src/LtiDeepLinkResource.php b/lib/lti1p3/src/LtiDeepLinkResource.php
deleted file mode 100644
index 093f979c84f..00000000000
--- a/lib/lti1p3/src/LtiDeepLinkResource.php
+++ /dev/null
@@ -1,243 +0,0 @@
-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;
- }
-}
diff --git a/lib/lti1p3/src/LtiDeepLinkResourceIcon.php b/lib/lti1p3/src/LtiDeepLinkResourceIcon.php
deleted file mode 100644
index f037910bc15..00000000000
--- a/lib/lti1p3/src/LtiDeepLinkResourceIcon.php
+++ /dev/null
@@ -1,67 +0,0 @@
-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,
- ];
- }
-}
diff --git a/lib/lti1p3/src/LtiDeepLinkResourceIframe.php b/lib/lti1p3/src/LtiDeepLinkResourceIframe.php
deleted file mode 100644
index e88b6934d27..00000000000
--- a/lib/lti1p3/src/LtiDeepLinkResourceIframe.php
+++ /dev/null
@@ -1,58 +0,0 @@
-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;
- }
-}
diff --git a/lib/lti1p3/src/LtiDeepLinkResourceWindow.php b/lib/lti1p3/src/LtiDeepLinkResourceWindow.php
deleted file mode 100644
index 323d706c17d..00000000000
--- a/lib/lti1p3/src/LtiDeepLinkResourceWindow.php
+++ /dev/null
@@ -1,92 +0,0 @@
-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;
- }
-}
diff --git a/lib/lti1p3/src/LtiDeployment.php b/lib/lti1p3/src/LtiDeployment.php
index cbe083aa196..7ac23989ed5 100644
--- a/lib/lti1p3/src/LtiDeployment.php
+++ b/lib/lti1p3/src/LtiDeployment.php
@@ -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;
diff --git a/lib/lti1p3/src/LtiGrade.php b/lib/lti1p3/src/LtiGrade.php
index 1f3ff4ee513..bd673b4e0a8 100644
--- a/lib/lti1p3/src/LtiGrade.php
+++ b/lib/lti1p3/src/LtiGrade.php
@@ -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;
diff --git a/lib/lti1p3/src/LtiGradeSubmissionReview.php b/lib/lti1p3/src/LtiGradeSubmissionReview.php
index a185930f2fb..4732d784008 100644
--- a/lib/lti1p3/src/LtiGradeSubmissionReview.php
+++ b/lib/lti1p3/src/LtiGradeSubmissionReview.php
@@ -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;
diff --git a/lib/lti1p3/src/LtiLineitem.php b/lib/lti1p3/src/LtiLineitem.php
index 8964ce19f3c..c8e662ed586 100644
--- a/lib/lti1p3/src/LtiLineitem.php
+++ b/lib/lti1p3/src/LtiLineitem.php
@@ -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;
+ }
}
diff --git a/lib/lti1p3/src/LtiMessageLaunch.php b/lib/lti1p3/src/LtiMessageLaunch.php
index d325e375068..62f2f794d61 100644
--- a/lib/lti1p3/src/LtiMessageLaunch.php
+++ b/lib/lti1p3/src/LtiMessageLaunch.php
@@ -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']
+ );
+ }
}
diff --git a/lib/lti1p3/src/LtiNamesRolesProvisioningService.php b/lib/lti1p3/src/LtiNamesRolesProvisioningService.php
index afad6d3a132..d3861e20349 100644
--- a/lib/lti1p3/src/LtiNamesRolesProvisioningService.php
+++ b/lib/lti1p3/src/LtiNamesRolesProvisioningService.php
@@ -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);
diff --git a/lib/lti1p3/src/LtiOidcLogin.php b/lib/lti1p3/src/LtiOidcLogin.php
index 7d12c7a51eb..7b9dc7f8a12 100644
--- a/lib/lti1p3/src/LtiOidcLogin.php
+++ b/lib/lti1p3/src/LtiOidcLogin.php
@@ -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
diff --git a/lib/lti1p3/src/LtiRegistration.php b/lib/lti1p3/src/LtiRegistration.php
index 21eebb7e300..4805c3bfb99 100644
--- a/lib/lti1p3/src/LtiRegistration.php
+++ b/lib/lti1p3/src/LtiRegistration.php
@@ -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;
diff --git a/lib/lti1p3/src/LtiServiceConnector.php b/lib/lti1p3/src/LtiServiceConnector.php
index ab69986d74a..90869535f6a 100644
--- a/lib/lti1p3/src/LtiServiceConnector.php
+++ b/lib/lti1p3/src/LtiServiceConnector.php
@@ -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);
diff --git a/lib/lti1p3/src/MessageValidators/AbstractMessageValidator.php b/lib/lti1p3/src/MessageValidators/AbstractMessageValidator.php
index 7c52929171a..c005196389f 100644
--- a/lib/lti1p3/src/MessageValidators/AbstractMessageValidator.php
+++ b/lib/lti1p3/src/MessageValidators/AbstractMessageValidator.php
@@ -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'])) {
diff --git a/lib/lti1p3/src/MessageValidators/DeepLinkMessageValidator.php b/lib/lti1p3/src/MessageValidators/DeepLinkMessageValidator.php
index e99a79164a5..67e007b007b 100644
--- a/lib/lti1p3/src/MessageValidators/DeepLinkMessageValidator.php
+++ b/lib/lti1p3/src/MessageValidators/DeepLinkMessageValidator.php
@@ -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);
diff --git a/lib/lti1p3/src/MessageValidators/ResourceMessageValidator.php b/lib/lti1p3/src/MessageValidators/ResourceMessageValidator.php
index e24c6b1133f..199e2cff905 100644
--- a/lib/lti1p3/src/MessageValidators/ResourceMessageValidator.php
+++ b/lib/lti1p3/src/MessageValidators/ResourceMessageValidator.php
@@ -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);
diff --git a/lib/lti1p3/src/MessageValidators/SubmissionReviewMessageValidator.php b/lib/lti1p3/src/MessageValidators/SubmissionReviewMessageValidator.php
index 45e63b57a13..6a1ef8922c3 100644
--- a/lib/lti1p3/src/MessageValidators/SubmissionReviewMessageValidator.php
+++ b/lib/lti1p3/src/MessageValidators/SubmissionReviewMessageValidator.php
@@ -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);
diff --git a/lib/lti1p3/src/Redirect.php b/lib/lti1p3/src/Redirect.php
deleted file mode 100644
index a87ed5f1274..00000000000
--- a/lib/lti1p3/src/Redirect.php
+++ /dev/null
@@ -1,84 +0,0 @@
-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()
- {
- ?>
- If you are not automatically redirected, click here to continue
-
- 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:';
diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml
index 8c299d240b2..0af34e5367d 100644
--- a/lib/thirdpartylibs.xml
+++ b/lib/thirdpartylibs.xml
@@ -437,7 +437,7 @@ All rights reserved.
lti1p3
LTI 1.3 Tool Library
A library used for building IMS-certified LTI 1.3 tool providers in PHP.
- 5.4.1
+ 6.0.0
Apache
2.0
https://github.com/packbackbooks/lti-1-3-php-library