Differences Between: [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]
1 <?php 2 3 namespace Packback\Lti1p3; 4 5 use Firebase\JWT\JWT; 6 use Packback\Lti1p3\Interfaces\ICache; 7 use Packback\Lti1p3\Interfaces\IHttpClient; 8 use Packback\Lti1p3\Interfaces\IHttpException; 9 use Packback\Lti1p3\Interfaces\IHttpResponse; 10 use Packback\Lti1p3\Interfaces\ILtiRegistration; 11 use Packback\Lti1p3\Interfaces\ILtiServiceConnector; 12 use Packback\Lti1p3\Interfaces\IServiceRequest; 13 14 class LtiServiceConnector implements ILtiServiceConnector 15 { 16 public const NEXT_PAGE_REGEX = '/<([^>]*)>; ?rel="next"/i'; 17 18 public const METHOD_GET = 'GET'; 19 public const METHOD_POST = 'POST'; 20 21 private $cache; 22 private $client; 23 private $debuggingMode = false; 24 25 public function __construct(ICache $cache, IHttpClient $client) 26 { 27 $this->cache = $cache; 28 $this->client = $client; 29 } 30 31 public function setDebuggingMode(bool $enable): void 32 { 33 $this->debuggingMode = $enable; 34 } 35 36 public function getAccessToken(ILtiRegistration $registration, array $scopes) 37 { 38 // Get a unique cache key for the access token 39 $accessTokenKey = $this->getAccessTokenCacheKey($registration, $scopes); 40 // Get access token from cache if it exists 41 $accessToken = $this->cache->getAccessToken($accessTokenKey); 42 if ($accessToken) { 43 return $accessToken; 44 } 45 46 // Build up JWT to exchange for an auth token 47 $clientId = $registration->getClientId(); 48 $jwtClaim = [ 49 'iss' => $clientId, 50 'sub' => $clientId, 51 'aud' => $registration->getAuthServer(), 52 'iat' => time() - 5, 53 'exp' => time() + 60, 54 'jti' => 'lti-service-token'.hash('sha256', random_bytes(64)), 55 ]; 56 57 // Sign the JWT with our private key (given by the platform on registration) 58 $jwt = JWT::encode($jwtClaim, $registration->getToolPrivateKey(), 'RS256', $registration->getKid()); 59 60 // Build auth token request headers 61 $authRequest = [ 62 'grant_type' => 'client_credentials', 63 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', 64 'client_assertion' => $jwt, 65 'scope' => implode(' ', $scopes), 66 ]; 67 68 $url = $registration->getAuthTokenUrl(); 69 70 // Get Access 71 $tokenRequest = new ServiceRequest('POST', $url); 72 $tokenRequest->setBody(http_build_query($authRequest, '', '&')); 73 $tokenRequest->setContentType('application/x-www-form-urlencoded'); 74 $tokenRequest->setAccept('application/json'); 75 $response = $this->client->request( 76 $tokenRequest->getMethod(), 77 $tokenRequest->getUrl(), 78 [ 79 'headers' => $tokenRequest->getPayload()['headers'], 80 'body' => $tokenRequest->getPayload()['body'] 81 ] 82 ); 83 $tokenData = $this->getResponseBody($response); 84 85 // Cache access token 86 $this->cache->cacheAccessToken($accessTokenKey, $tokenData['access_token']); 87 88 return $tokenData['access_token']; 89 } 90 91 92 public function makeRequest(IServiceRequest $request) 93 { 94 return $this->client->request( 95 $request->getMethod(), 96 $request->getUrl(), 97 $request->getPayload() 98 ); 99 } 100 101 public function getResponseBody(IHttpResponse $response): ?array 102 { 103 $responseBody = (string) $response->getBody(); 104 105 return json_decode($responseBody, true); 106 } 107 108 public function makeServiceRequest( 109 ILtiRegistration $registration, 110 array $scopes, 111 IServiceRequest $request, 112 bool $shouldRetry = true 113 ): array { 114 $request->setAccessToken($this->getAccessToken($registration, $scopes)); 115 try { 116 $response = $this->makeRequest($request); 117 } catch (IHttpException $e) { 118 $status = $e->getResponse()->getStatusCode(); 119 // If the error was due to invalid authentication and the request 120 // should be retried, clear the access token and retry it. 121 if ($status === 401 && $shouldRetry) { 122 $key = $this->getAccessTokenCacheKey($registration, $scopes); 123 $this->cache->clearAccessToken($key); 124 125 return $this->makeServiceRequest($registration, $scopes, $request, false); 126 } 127 throw $e; 128 } 129 130 $responseHeaders = $response->getHeaders(); 131 $responseBody = $this->getResponseBody($response); 132 133 if ($this->debuggingMode) { 134 error_log('Syncing grade for this lti_user_id: '. 135 json_decode($request->getPayload()['body'])->userId.' '.print_r([ 136 'request_method' => $request->getMethod(), 137 'request_url' => $request->getUrl(), 138 'request_body' => $request->getPayload()['body'], 139 'response_headers' => $responseHeaders, 140 'response_body' => json_encode($responseBody), 141 ], true)); 142 } 143 144 return [ 145 'headers' => $responseHeaders, 146 'body' => $responseBody, 147 'status' => $response->getStatusCode(), 148 ]; 149 } 150 151 public function getAll( 152 ILtiRegistration $registration, 153 array $scopes, 154 IServiceRequest $request, 155 string $key = null 156 ): array { 157 if ($request->getMethod() !== static::METHOD_GET) { 158 throw new \Exception('An invalid method was specified by an LTI service requesting all items.'); 159 } 160 161 $results = []; 162 $nextUrl = $request->getUrl(); 163 164 while ($nextUrl) { 165 $response = $this->makeServiceRequest($registration, $scopes, $request); 166 167 $page_results = $key === null ? ($response['body'] ?? []) : ($response['body'][$key] ?? []); 168 $results = array_merge($results, $page_results); 169 170 $nextUrl = $this->getNextUrl($response['headers']); 171 if ($nextUrl) { 172 $request->setUrl($nextUrl); 173 } 174 } 175 176 return $results; 177 } 178 179 private function getAccessTokenCacheKey(ILtiRegistration $registration, array $scopes) 180 { 181 sort($scopes); 182 $scopeKey = md5(implode('|', $scopes)); 183 184 return $registration->getIssuer().$registration->getClientId().$scopeKey; 185 } 186 187 private function getNextUrl(array $headers) 188 { 189 $subject = $headers['Link'] ?? ''; 190 preg_match(LtiServiceConnector::NEXT_PAGE_REGEX, $subject, $matches); 191 192 return $matches[1] ?? null; 193 } 194 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body