• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

packbackbooks / lti-1-3-php-library / 16545210592

25 Jul 2025 03:48PM UTC coverage: 97.702%. Remained the same
16545210592

push

github

web-flow
Merge pull request #165 from packbackbooks/mgmt-194-cleanup

Update linting rules

80 of 86 new or added lines in 6 files covered. (93.02%)

19 existing lines in 4 files now uncovered.

1148 of 1175 relevant lines covered (97.7%)

6.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.52
/src/LtiServiceConnector.php
1
<?php
2

3
namespace Packback\Lti1p3;
4

5
use Exception;
6
use Firebase\JWT\JWT;
7
use GuzzleHttp\Client;
8
use GuzzleHttp\Exception\ClientException;
9
use Packback\Lti1p3\Interfaces\ICache;
10
use Packback\Lti1p3\Interfaces\ILtiRegistration;
11
use Packback\Lti1p3\Interfaces\ILtiServiceConnector;
12
use Packback\Lti1p3\Interfaces\IServiceRequest;
13
use Psr\Http\Message\ResponseInterface;
14

15
class LtiServiceConnector implements ILtiServiceConnector
16
{
17
    public const NEXT_PAGE_REGEX = '/<([^>]*)>; ?rel="next"/i';
18
    private bool $debuggingMode = false;
19

20
    public function __construct(
9✔
21
        private ICache $cache,
22
        private Client $client
23
    ) {}
9✔
24

25
    public static function getLogMessage(
2✔
26
        IServiceRequest $request,
27
        array $responseHeaders,
28
        ?array $responseBody
29
    ): string {
30
        if ($request->getMaskResponseLogs()) {
2✔
31
            $responseHeaders = self::maskValues($responseHeaders);
1✔
32
            $responseBody = self::maskValues($responseBody);
1✔
33
        }
34

35
        $contextArray = [
2✔
36
            'request_method' => $request->getMethod(),
2✔
37
            'request_url' => $request->getUrl(),
2✔
38
            'response_headers' => $responseHeaders,
2✔
39
            'response_body' => $responseBody,
2✔
40
        ];
2✔
41

42
        $requestBody = $request->getPayload()['body'] ?? null;
2✔
43

44
        if (isset($requestBody)) {
2✔
45
            $contextArray['request_body'] = $requestBody;
2✔
46
        }
47

48
        return implode(' ', array_filter([
2✔
49
            $request->getErrorPrefix(),
2✔
50
            json_decode($requestBody)->userId ?? null,
2✔
51
            json_encode($contextArray),
2✔
52
        ]));
2✔
53
    }
54

55
    private static function maskValues(?array $payload): ?array
1✔
56
    {
57
        if (!isset($payload) || empty($payload)) {
1✔
NEW
58
            return $payload;
×
59
        }
60

61
        foreach ($payload as $key => $value) {
1✔
62
            $payload[$key] = '***';
1✔
63
        }
64

65
        return $payload;
1✔
66
    }
67

UNCOV
68
    public function setDebuggingMode(bool $enable): self
×
69
    {
UNCOV
70
        $this->debuggingMode = $enable;
×
71

UNCOV
72
        return $this;
×
73
    }
74

75
    public function getAccessToken(ILtiRegistration $registration, array $scopes): string
6✔
76
    {
77
        // Get a unique cache key for the access token
78
        $accessTokenKey = $this->getAccessTokenCacheKey($registration, $scopes);
6✔
79
        // Get access token from cache if it exists
80
        $accessToken = $this->cache->getAccessToken($accessTokenKey);
6✔
81

82
        if (isset($accessToken)) {
6✔
83
            return $accessToken;
5✔
84
        }
85

86
        // Build up JWT to exchange for an auth token
87
        $clientId = $registration->getClientId();
1✔
88
        $jwtClaim = [
1✔
89
            'iss' => $clientId,
1✔
90
            'sub' => $clientId,
1✔
91
            'aud' => [$registration->getAuthTokenUrl()],
1✔
92
            'iat' => time() - 5,
1✔
93
            'exp' => time() + 60,
1✔
94
            'jti' => 'lti-service-token'.hash('sha256', random_bytes(64)),
1✔
95
        ];
1✔
96

97
        // Sign the JWT with our private key (given by the platform on registration)
98
        $jwt = JWT::encode($jwtClaim, $registration->getToolPrivateKey(), 'RS256', $registration->getKid());
1✔
99

100
        // Build auth token request headers
101
        $authRequest = [
1✔
102
            'grant_type' => 'client_credentials',
1✔
103
            'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
1✔
104
            'client_assertion' => $jwt,
1✔
105
            'scope' => implode(' ', $scopes),
1✔
106
        ];
1✔
107

108
        // Get Access
109
        $request = new ServiceRequest(
1✔
110
            ServiceRequest::METHOD_POST,
1✔
111
            $registration->getAuthTokenUrl(),
1✔
112
            ServiceRequest::TYPE_AUTH
1✔
113
        );
1✔
114
        $request->setPayload(['form_params' => $authRequest])
1✔
115
            ->setMaskResponseLogs(true);
1✔
116
        $response = $this->makeRequest($request);
1✔
117

118
        $tokenData = $this->getResponseBody($response);
1✔
119

120
        // Cache access token
121
        $this->cache->cacheAccessToken($accessTokenKey, $tokenData['access_token']);
1✔
122

123
        return $tokenData['access_token'];
1✔
124
    }
125

126
    public function makeRequest(IServiceRequest $request): ResponseInterface
5✔
127
    {
128
        $response = $this->client->request(
5✔
129
            $request->getMethod(),
5✔
130
            $request->getUrl(),
5✔
131
            $request->getPayload()
5✔
132
        );
5✔
133

134
        if ($this->debuggingMode) {
4✔
UNCOV
135
            $this->logRequest(
×
UNCOV
136
                $request,
×
UNCOV
137
                $this->getResponseHeaders($response),
×
UNCOV
138
                $this->getResponseBody($response)
×
UNCOV
139
            );
×
140
        }
141

142
        return $response;
4✔
143
    }
144

145
    public function getResponseHeaders(ResponseInterface $response): ?array
3✔
146
    {
147
        $responseHeaders = $response->getHeaders();
3✔
148
        array_walk($responseHeaders, function (&$value) {
3✔
149
            $value = $value[0];
3✔
150
        });
3✔
151

152
        return $responseHeaders;
3✔
153
    }
154

155
    public function getResponseBody(ResponseInterface $response): ?array
4✔
156
    {
157
        $responseBody = (string) $response->getBody();
4✔
158

159
        return json_decode($responseBody, true);
4✔
160
    }
161

162
    public function makeServiceRequest(
4✔
163
        ILtiRegistration $registration,
164
        array $scopes,
165
        IServiceRequest $request,
166
        bool $shouldRetry = true
167
    ): array {
168
        $request->setAccessToken($this->getAccessToken($registration, $scopes));
4✔
169

170
        try {
171
            $response = $this->makeRequest($request);
4✔
172
        } catch (ClientException $e) {
2✔
173
            $status = $e->getResponse()->getStatusCode();
2✔
174

175
            // If the error was due to invalid authentication and the request
176
            // should be retried, clear the access token and retry it.
177
            if ($status === 401 && $shouldRetry) {
2✔
178
                $key = $this->getAccessTokenCacheKey($registration, $scopes);
2✔
179
                $this->cache->clearAccessToken($key);
2✔
180

181
                return $this->makeServiceRequest($registration, $scopes, $request, false);
2✔
182
            }
183

184
            throw $e;
1✔
185
        }
186

187
        return [
3✔
188
            'headers' => $this->getResponseHeaders($response),
3✔
189
            'body' => $this->getResponseBody($response),
3✔
190
            'status' => $response->getStatusCode(),
3✔
191
        ];
3✔
192
    }
193

194
    public function getAll(
1✔
195
        ILtiRegistration $registration,
196
        array $scopes,
197
        IServiceRequest $request,
198
        ?string $key = null
199
    ): array {
200
        if ($request->getMethod() !== ServiceRequest::METHOD_GET) {
1✔
UNCOV
201
            throw new Exception('An invalid method was specified by an LTI service requesting all items.');
×
202
        }
203

204
        $results = [];
1✔
205
        $nextUrl = $request->getUrl();
1✔
206

207
        while ($nextUrl) {
1✔
208
            $request->setUrl($nextUrl);
1✔
209
            $response = $this->makeServiceRequest($registration, $scopes, $request);
1✔
210
            $pageResults = $this->getResultsFromResponse($response, $key);
1✔
211
            $results = array_merge($results, $pageResults);
1✔
212
            $nextUrl = $this->getNextUrl($response['headers']);
1✔
213
        }
214

215
        return $results;
1✔
216
    }
217

UNCOV
218
    private function logRequest(
×
219
        IServiceRequest $request,
220
        array $responseHeaders,
221
        ?array $responseBody
222
    ): void {
UNCOV
223
        error_log(self::getLogMessage($request, $responseHeaders, $responseBody));
×
224
    }
225

226
    private function getAccessTokenCacheKey(ILtiRegistration $registration, array $scopes): string
6✔
227
    {
228
        sort($scopes);
6✔
229
        $scopeKey = md5(implode('|', $scopes));
6✔
230

231
        return $registration->getIssuer().$registration->getClientId().$scopeKey;
6✔
232
    }
233

234
    private function getResultsFromResponse(array $response, ?string $key = null): array
1✔
235
    {
236
        if (isset($key)) {
1✔
237
            return $response['body'][$key] ?? [];
1✔
238
        }
239

240
        return $response['body'] ?? [];
×
241
    }
242

243
    private function getNextUrl(array $headers): ?string
1✔
244
    {
245
        $subject = $headers['Link'] ?? $headers['link'] ?? '';
1✔
246
        preg_match(static::NEXT_PAGE_REGEX, $subject, $matches);
1✔
247

248
        return $matches[1] ?? null;
1✔
249
    }
250
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc