Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
<?php

namespace Packback\Lti1p3;

> use Exception;
use Firebase\JWT\JWT;
> use GuzzleHttp\Client; use Packback\Lti1p3\Interfaces\ICache; > use GuzzleHttp\Exception\ClientException; use Packback\Lti1p3\Interfaces\IHttpClient; > use GuzzleHttp\Psr7\Response;
< use Packback\Lti1p3\Interfaces\IHttpClient; < use Packback\Lti1p3\Interfaces\IHttpException; < use Packback\Lti1p3\Interfaces\IHttpResponse;
use Packback\Lti1p3\Interfaces\ILtiServiceConnector; use Packback\Lti1p3\Interfaces\IServiceRequest; class LtiServiceConnector implements ILtiServiceConnector { public const NEXT_PAGE_REGEX = '/<([^>]*)>; ?rel="next"/i';
< < public const METHOD_GET = 'GET'; < public const METHOD_POST = 'POST'; <
private $cache; private $client; private $debuggingMode = false;
< public function __construct(ICache $cache, IHttpClient $client) < {
> public function __construct( > ICache $cache, > Client $client > ) {
$this->cache = $cache; $this->client = $client; } public function setDebuggingMode(bool $enable): void { $this->debuggingMode = $enable; } public function getAccessToken(ILtiRegistration $registration, array $scopes) { // 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) { return $accessToken; } // Build up JWT to exchange for an auth token $clientId = $registration->getClientId(); $jwtClaim = [ 'iss' => $clientId, 'sub' => $clientId, 'aud' => $registration->getAuthServer(), 'iat' => time() - 5, 'exp' => time() + 60, 'jti' => 'lti-service-token'.hash('sha256', random_bytes(64)), ]; // Sign the JWT with our private key (given by the platform on registration) $jwt = JWT::encode($jwtClaim, $registration->getToolPrivateKey(), 'RS256', $registration->getKid()); // Build auth token request headers $authRequest = [ 'grant_type' => 'client_credentials', 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', 'client_assertion' => $jwt, 'scope' => implode(' ', $scopes), ];
< $url = $registration->getAuthTokenUrl(); <
// Get Access
< $tokenRequest = new ServiceRequest('POST', $url); < $tokenRequest->setBody(http_build_query($authRequest, '', '&')); < $tokenRequest->setContentType('application/x-www-form-urlencoded'); < $tokenRequest->setAccept('application/json'); < $response = $this->client->request( < $tokenRequest->getMethod(), < $tokenRequest->getUrl(), < [ < 'headers' => $tokenRequest->getPayload()['headers'], < 'body' => $tokenRequest->getPayload()['body'] < ]
> $request = new ServiceRequest( > ServiceRequest::METHOD_POST, > $registration->getAuthTokenUrl(), > ServiceRequest::TYPE_AUTH
);
> $request->setPayload(['form_params' => $authRequest]); $tokenData = $this->getResponseBody($response); > $response = $this->makeRequest($request); >
// Cache access token $this->cache->cacheAccessToken($accessTokenKey, $tokenData['access_token']); return $tokenData['access_token']; }
<
public function makeRequest(IServiceRequest $request) {
< return $this->client->request(
> $response = $this->client->request(
$request->getMethod(), $request->getUrl(), $request->getPayload() );
> } > if ($this->debuggingMode) { > $this->logRequest( public function getResponseBody(IHttpResponse $response): ?array > $request, { > $this->getResponseHeaders($response), $responseBody = (string) $response->getBody(); > $this->getResponseBody($response) > ); return json_decode($responseBody, true); > } } > > return $response;
< public function getResponseBody(IHttpResponse $response): ?array
> public function getResponseHeaders(Response $response): ?array > { > $responseHeaders = $response->getHeaders(); > array_walk($responseHeaders, function (&$value) { > $value = $value[0]; > }); > > return $responseHeaders; > } > > public function getResponseBody(Response $response): ?array
ILtiRegistration $registration, array $scopes, IServiceRequest $request, bool $shouldRetry = true ): array { $request->setAccessToken($this->getAccessToken($registration, $scopes));
>
try { $response = $this->makeRequest($request);
< } catch (IHttpException $e) {
> } catch (ClientException $e) {
$status = $e->getResponse()->getStatusCode();
>
// If the error was due to invalid authentication and the request // should be retried, clear the access token and retry it. if ($status === 401 && $shouldRetry) { $key = $this->getAccessTokenCacheKey($registration, $scopes); $this->cache->clearAccessToken($key); return $this->makeServiceRequest($registration, $scopes, $request, false); }
< throw $e; < } < < $responseHeaders = $response->getHeaders(); < $responseBody = $this->getResponseBody($response);
< if ($this->debuggingMode) { < error_log('Syncing grade for this lti_user_id: '. < json_decode($request->getPayload()['body'])->userId.' '.print_r([ < 'request_method' => $request->getMethod(), < 'request_url' => $request->getUrl(), < 'request_body' => $request->getPayload()['body'], < 'response_headers' => $responseHeaders, < 'response_body' => json_encode($responseBody), < ], true));
> throw $e;
} return [
< 'headers' => $responseHeaders, < 'body' => $responseBody,
> 'headers' => $this->getResponseHeaders($response), > 'body' => $this->getResponseBody($response),
'status' => $response->getStatusCode(), ]; } public function getAll( ILtiRegistration $registration, array $scopes, IServiceRequest $request, string $key = null ): array {
< if ($request->getMethod() !== static::METHOD_GET) { < throw new \Exception('An invalid method was specified by an LTI service requesting all items.');
> if ($request->getMethod() !== ServiceRequest::METHOD_GET) { > throw new Exception('An invalid method was specified by an LTI service requesting all items.');
} $results = []; $nextUrl = $request->getUrl(); while ($nextUrl) { $response = $this->makeServiceRequest($registration, $scopes, $request); $page_results = $key === null ? ($response['body'] ?? []) : ($response['body'][$key] ?? []); $results = array_merge($results, $page_results); $nextUrl = $this->getNextUrl($response['headers']); if ($nextUrl) { $request->setUrl($nextUrl); } } return $results; }
> private function logRequest( private function getAccessTokenCacheKey(ILtiRegistration $registration, array $scopes) > IServiceRequest $request, { > array $responseHeaders, sort($scopes); > ?array $responseBody $scopeKey = md5(implode('|', $scopes)); > ): void { > $contextArray = [ return $registration->getIssuer().$registration->getClientId().$scopeKey; > 'request_method' => $request->getMethod(), } > 'request_url' => $request->getUrl(), > 'response_headers' => $responseHeaders, private function getNextUrl(array $headers) > 'response_body' => json_encode($responseBody), { > ]; $subject = $headers['Link'] ?? ''; > preg_match(LtiServiceConnector::NEXT_PAGE_REGEX, $subject, $matches); > $requestBody = $request->getPayload()['body'] ?? null; > return $matches[1] ?? null; > if (!empty($requestBody)) { } > $contextArray['request_body'] = $requestBody; } > } > > error_log(implode(' ', array_filter([ > $request->getErrorPrefix(), > json_decode($requestBody)->userId ?? null, > print_r($contextArray, true), > ]))); > } >
< $subject = $headers['Link'] ?? ''; < preg_match(LtiServiceConnector::NEXT_PAGE_REGEX, $subject, $matches);
> $subject = $headers['Link'] ?? $headers['link'] ?? ''; > preg_match(static::NEXT_PAGE_REGEX, $subject, $matches);