ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
XapiProxyRequest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
21namespace XapiProxy;
22
23use GuzzleHttp\Client;
24use GuzzleHttp\Promise;
25use GuzzleHttp\RequestOptions;
26use GuzzleHttp\Psr7\Request;
27use GuzzleHttp\Psr7\Uri;
29
31{
32 private Container $dic;
33
35
37
38 private string $cmdPart2plus = "";
39 private bool $checkGetStatements = true;
40
42 {
43 $this->dic = $GLOBALS['DIC'];
44 $this->xapiproxy = $xapiproxy;
45 $this->cmdPart2plus = "";
46 }
47
48 public function handle(): void
49 {
50 $this->xapiProxyResponse = $this->xapiproxy->getXapiProxyResponse();
51 $request = $this->dic->http()->request();
52 $cmdParts = $this->xapiproxy->cmdParts();
53 $this->xapiproxy->log()->debug($this->msg(var_export($cmdParts, true)));
54 if (count($cmdParts) === 5) {
55 $cmd = $cmdParts[3];
56 if ($cmd === "statements") {
57 $this->handleStatementsRequest($request);
58 } elseif ($cmd === "activities") {
59 $this->handleActivitiesRequest($request);
60 } elseif ($cmd === "activities/profile") {
61 $this->handleActivitiesProfileRequest($request);
62 } elseif ($cmd === "activities/state") {
63 $this->handleActivitiesStateRequest($request);
64 } elseif ($cmd === "agents") {
65 $this->handleAgentsRequest($request);
66 } elseif ($cmd === "agents/profile") {
67 $this->handleAgentsProfileRequest($request);
68 } elseif ($cmd === "about") {
69 $this->handleAboutRequest($request);
70 } else {
71 $this->xapiproxy->log()->debug($this->msg("Wrong xApi Query: " . $request->getUri()));
72 $this->xapiProxyResponse->exitBadRequest();
73 }
74 } else {
75 $this->xapiproxy->log()->error($this->msg("Wrong xApi Query: " . $request->getUri()));
76 $this->xapiProxyResponse->exitBadRequest();
77 }
78 }
79
80 private function msg(string $msg): string
81 {
82 return $this->xapiproxy->msg($msg);
83 }
84
85 private function handleStatementsRequest(\Psr\Http\Message\RequestInterface $request): void
86 {
87 $this->xapiproxy->log()->debug($this->msg("handleStatementsRequest (" . $this->xapiproxy->method() . "): " . $request->getUri()));
88 $method = $this->xapiproxy->method();
89 if ($method === "post" || $method === "put") {
90 $this->handlePostPutStatementsRequest($request);
91 } elseif ($method === "get") {
92 $this->handleGetStatementsRequest($request);
93 } else {
94 $this->xapiProxyResponse->exitBadRequest();
95 }
96 }
97
98 private function handleGetStatementsRequest(\Psr\Http\Message\RequestInterface $request): void
99 {
100 if ($this->xapiproxy->cmdParts()[4] == "") {
101 $this->xapiproxy->log()->warning($this->msg("unfiltered get statements requests are not allowed for security reasons"));
102 $this->xapiProxyResponse->exitBadRequest();
103 }
104 $this->xapiproxy->log()->debug($this->msg("handleGetStatementsRequest: " . $request->getUri()));
105
106 try {
107 $badRequest = false;
108 if ($this->checkGetStatements) {
109 $authToken = \ilCmiXapiAuthToken::getInstanceByToken($this->xapiproxy->token());
110 $obj = \ilObjCmiXapi::getInstance($authToken->getRefId(), true);
111 $access = \ilCmiXapiAccess::getInstance($obj);
112 $params = $this->dic->http()->wrapper()->query();
113 if ($params->has('statementId')) {
114 $this->xapiproxy->log()->debug($this->msg("single statementId requests can not be secured. It is not allowed to append any additional parameter like registration or activity (tested in LL7)"));
115 // single statementId can not be handled. it is not allowed to append a registration on single statement requests (tested in LL7)
116 $this->handleProxy($request);
117 } else {
118 if ($params->has('activity')) {
119 // ToDo: how this can be verified? the object only knows the top activityId
120 $this->handleProxy($request);
121 } else {
122 $this->xapiproxy->log()->debug($this->msg("add activity: " . $obj->getActivityId()));
123 $this->cmdPart2plus .= "&activity=" . $obj->getActivityId() . "&related_activities=true";
124 }
125 if (!$access->hasOutcomesAccess($authToken->getUsrId())) {
126 // ToCheck
127 /*
128 if (!$access->hasStatementsAccess()) {
129 $this->xapiproxy->log()->warning($this->msg("statements access is not enabled"));
130 $this->xapiProxyResponse->exitBadRequest();
131 }
132 */
133 if ($obj->getContentType() == \ilObjCmiXapi::CONT_TYPE_CMI5) {
134 $regUserObject = \ilCmiXapiUser::getCMI5RegistrationFromAuthToken($authToken);
135 } else {
136 $regUserObject = \ilCmiXapiUser::getRegistrationFromAuthToken($authToken);
137 }
138 if ($params->has('registration')) {
139 $regParam = $params->retrieve('registration', $this->dic->refinery()->kindlyTo()->string());
140 if ($regParam != $regUserObject) {
141 $this->xapiproxy->log()->debug($this->msg("wrong registration: " . $regParam . " != " . $regUserObject));
142 $badRequest = true;
143 }
144 } else { // add registration
145 $this->xapiproxy->log()->debug($this->msg("add registration: " . $regUserObject));
146 $this->cmdPart2plus .= "&registration=" . $regUserObject;
147 }
148 }
149 }
150 }
151 if ($badRequest) {
152 $this->xapiProxyResponse->exitBadRequest();
153 } else {
154 $this->handleProxy($request);
155 }
156 } catch (\Exception $e) {
157 $this->xapiproxy->log()->error($this->msg($e->getMessage()));
158 }
159 }
160
161 private function handlePostPutStatementsRequest(\Psr\Http\Message\RequestInterface $request): void
162 {
163 $this->xapiproxy->log()->debug($this->msg("handlePostPutStatementsRequest: " . $request->getUri()));
164 $body = $request->getBody()->getContents();
165 $fakePostBody = null;
166 if (empty($body)) {
167 $this->xapiproxy->log()->warning($this->msg("empty body in handlePostPutRequest"));
168 $this->handleProxy($request);
169 } else {
170 try {
171 $this->xapiproxy->log()->debug($this->msg("process statements"));
172 $retArr = $this->xapiproxy->processStatements($request, $body);
173 if (is_array($retArr)) {
174 $body = json_encode($retArr[0]); // new body with allowed statements
175 $fakePostBody = $retArr[1]; // fake post php array of ALL statments as if all statements were processed
176 }
177 } catch (\Exception $e) {
178 $this->xapiproxy->log()->error($this->msg($e->getMessage()));
179 $this->xapiProxyResponse->exitProxyError();
180 }
181 try {
182 $body = $this->xapiproxy->modifyBody($body);
183 $req = new Request($request->getMethod(), $request->getUri(), $request->getHeaders(), $body);
184 $this->handleProxy($req, $fakePostBody);
185 } catch (\Exception $e) {
186 $this->xapiproxy->log()->error($this->msg($e->getMessage()));
187 $this->handleProxy($request, $fakePostBody);
188 }
189 }
190 }
191
192 private function handleActivitiesRequest(\Psr\Http\Message\RequestInterface $request): void
193 {
194 // $this->xapiproxy->log()->debug($this->msg("blocked handleActivitiesRequest (" . $this->xapiproxy->method() . "): " . $request->getUri()));
195 // $this->xapiProxyResponse->exitBadRequest();
196 $this->xapiproxy->log()->debug($this->msg("handleActivitiesRequest (" . $this->xapiproxy->method() . "): " . $request->getUri()));
197 $this->handleProxy($request);
198 }
199
200 private function handleActivitiesProfileRequest(\Psr\Http\Message\RequestInterface $request): void
201 {
202 $this->xapiproxy->log()->debug($this->msg("handleActivitiesProfileRequest (" . $this->xapiproxy->method() . "): " . $request->getUri()));
203 $this->handleProxy($request);
204 }
205
206 private function handleActivitiesStateRequest(\Psr\Http\Message\RequestInterface $request): void
207 {
208 $this->xapiproxy->log()->debug($this->msg("handleActivitiesStateRequest (" . $this->xapiproxy->method() . "): " . $request->getUri()));
209 $this->handleProxy($request);
210 }
211
212 private function handleAgentsRequest(\Psr\Http\Message\RequestInterface $request): void
213 {
214 $this->xapiproxy->log()->debug($this->msg("blocked handleAgentsRequest (" . $this->xapiproxy->method() . "): " . $request->getUri()));
215 $this->xapiProxyResponse->exitBadRequest();
216 }
217
218 private function handleAgentsProfileRequest(\Psr\Http\Message\RequestInterface $request): void
219 {
220 $this->xapiproxy->log()->debug($this->msg("handleAgentsProfileRequest (" . $this->xapiproxy->method() . "): " . $request->getUri()));
221 $this->handleProxy($request);
222 }
223
224 private function handleAboutRequest(\Psr\Http\Message\RequestInterface $request): void
225 {
226 $this->xapiproxy->log()->debug($this->msg("handleAboutRequest (" . $this->xapiproxy->method() . "): " . $request->getUri()));
227 $this->handleProxy($request);
228 }
229
230 private function handleProxy(\Psr\Http\Message\RequestInterface $request, $fakePostBody = null): void
231 {
232 $endpointDefault = $this->xapiproxy->getDefaultLrsEndpoint();
233 $endpointFallback = $this->xapiproxy->getFallbackLrsEndpoint();
234
235 $this->xapiproxy->log()->debug($this->msg("endpointDefault: " . $endpointDefault));
236 $this->xapiproxy->log()->debug($this->msg("endpointFallback: " . $endpointFallback));
237
238 $keyDefault = $this->xapiproxy->getDefaultLrsKey();
239 $secretDefault = $this->xapiproxy->getDefaultLrsSecret();
240 $authDefault = 'Basic ' . base64_encode($keyDefault . ':' . $secretDefault);
241
242 $hasFallback = ($endpointFallback === "") ? false : true;
243
244 if ($hasFallback) {
245 $keyFallback = $this->xapiproxy->getFallbackLrsKey();
246 $secretFallback = $this->xapiproxy->getFallbackLrsSecret();
247 $authFallback = 'Basic ' . base64_encode($keyFallback . ':' . $secretFallback);
248 }
249
250 $req_opts = array(
251 RequestOptions::VERIFY => true,
252 RequestOptions::CONNECT_TIMEOUT => 10,
253 RequestOptions::HTTP_ERRORS => false
254 );
255 $cmd = $this->xapiproxy->cmdParts()[2] . $this->cmdPart2plus;
256 $upstreamDefault = $endpointDefault . $cmd;
257 $uriDefault = new Uri($upstreamDefault);
258 $body = $request->getBody()->getContents();
259 $reqDefault = $this->createProxyRequest($request, $uriDefault, $authDefault, $body);
260
261 if ($hasFallback) {
262 $upstreamFallback = $endpointFallback . $cmd;
263 $uriFallback = new Uri($upstreamFallback);
264 $reqFallback = $this->createProxyRequest($request, $uriFallback, $authFallback, $body);
265 }
266
267 $httpclient = new Client();
268 if ($hasFallback) {
269 $promises = [
270 'default' => $httpclient->sendAsync($reqDefault, $req_opts),
271 'fallback' => $httpclient->sendAsync($reqFallback, $req_opts)
272 ];
273
274 // this would throw first ConnectionException
275 // $responses = Promise\unwrap($promises);
276 try {
277 $responses = Promise\Utils::settle($promises)->wait();
278 } catch (\Exception $e) {
279 $this->xapiproxy->log()->error($this->msg($e->getMessage()));
280 }
281
282 $defaultOk = $this->xapiProxyResponse->checkResponse($responses['default'], $endpointDefault);
283 $fallbackOk = $this->xapiProxyResponse->checkResponse($responses['fallback'], $endpointFallback);
284
285 if ($defaultOk) {
286 try {
287 $this->xapiProxyResponse->handleResponse(
288 $reqDefault,
289 $responses['default']['value'],
290 $fakePostBody
291 );
292 } catch (\Exception $e) {
293 // $this->xapiproxy->error($this->msg("XAPI exception from Default LRS: " . $endpointDefault . " (sent HTTP 500 to client): " . $e->getMessage()));
294 $this->xapiProxyResponse->exitProxyError();
295 }
296 } elseif ($fallbackOk) {
297 try {
298 $this->xapiProxyResponse->handleResponse(
299 $reqFallback,
300 $responses['fallback']['value'],
301 $fakePostBody
302 );
303 } catch (\Exception $e) {
304 // $this->xapiproxy->error($this->msg("XAPI exception from Default LRS: " . $endpointDefault . " (sent HTTP 500 to client): " . $e->getMessage()));
305 $this->xapiProxyResponse->exitProxyError();
306 }
307 } else {
308 $this->xapiProxyResponse->exitResponseError();
309 }
310 } else {
311 $promises = [
312 'default' => $httpclient->sendAsync($reqDefault, $req_opts)
313 ];
314 // this would throw first ConnectionException
315 // $responses = Promise\unwrap($promises);
316 try {
317 $responses = Promise\Utils::settle($promises)->wait();
318 } catch (\Exception $e) {
319 $this->xapiproxy->log()->error($this->msg($e->getMessage()));
320 }
321 if ($this->xapiProxyResponse->checkResponse($responses['default'], $endpointDefault)) {
322 try {
323 $this->xapiProxyResponse->handleResponse(
324 $reqDefault,
325 $responses['default']['value'],
326 $fakePostBody
327 );
328 } catch (\Exception $e) {
329 // $this->xapiproxy->error($this->msg("XAPI exception from Default LRS: " . $endpointDefault . " (sent HTTP 500 to client): " . $e->getMessage()));
330 $this->xapiProxyResponse->exitProxyError();
331 }
332 } else {
333 $this->xapiProxyResponse->exitResponseError();
334 }
335 }
336 }
337
338 private function createProxyRequest(\Psr\Http\Message\RequestInterface $request, \GuzzleHttp\Psr7\Uri $uri, string $auth, string $body): \GuzzleHttp\Psr7\Request
339 {
340 $headers = array(
341 'Cache-Control' => 'no-cache, no-store, must-revalidate',
342 'Authorization' => $auth
343 );
344
345 if ($request->hasHeader('X-Experience-API-Version')) {
346 $headers['X-Experience-API-Version'] = $request->getHeader('X-Experience-API-Version');
347 }
348
349 if ($request->hasHeader('Referrer')) {
350 $headers['Referrer'] = $request->getHeader('Referrer');
351 }
352
353 if ($request->hasHeader('Content-Type')) {
354 $headers['Content-Type'] = $request->getHeader('Content-Type');
355 }
356
357 if ($request->hasHeader('Origin')) {
358 $headers['Origin'] = $request->getHeader('Origin');
359 }
360
361 if ($request->hasHeader('Content-Length')) {
362 $contentLength = $request->getHeader('Content-Length');
363 if (is_array($contentLength) && $contentLength[0] === '') {
364 $contentLength = array(0);
365 } elseif ($contentLength === '') {
366 $contentLength = array(0);
367 }
368 $headers['Content-Length'] = $contentLength;
369 }
370
371 if ($request->hasHeader('Connection')) {
372 $headers['Connection'] = $request->getHeader('Connection');
373 }
374
375 //$this->xapiproxy->log()->debug($this->msg($body));
376
377 $req = new Request(strtoupper($request->getMethod()), $uri, $headers, $body);
378
379 return $req;
380 }
381}
Customizing of pimple-DIC for ILIAS.
Definition: Container.php:36
__construct(XapiProxy $xapiproxy)
XapiProxyResponse $xapiProxyResponse
handleActivitiesStateRequest(\Psr\Http\Message\RequestInterface $request)
handlePostPutStatementsRequest(\Psr\Http\Message\RequestInterface $request)
handleAboutRequest(\Psr\Http\Message\RequestInterface $request)
handleStatementsRequest(\Psr\Http\Message\RequestInterface $request)
handleActivitiesProfileRequest(\Psr\Http\Message\RequestInterface $request)
handleActivitiesRequest(\Psr\Http\Message\RequestInterface $request)
handleGetStatementsRequest(\Psr\Http\Message\RequestInterface $request)
handleAgentsRequest(\Psr\Http\Message\RequestInterface $request)
handleAgentsProfileRequest(\Psr\Http\Message\RequestInterface $request)
handleProxy(\Psr\Http\Message\RequestInterface $request, $fakePostBody=null)
createProxyRequest(\Psr\Http\Message\RequestInterface $request, \GuzzleHttp\Psr7\Uri $uri, string $auth, string $body)
static getInstance(ilObjCmiXapi $object)
static getInstanceByToken(string $token)
static getCMI5RegistrationFromAuthToken(ilCmiXapiAuthToken $authToken)
static getRegistrationFromAuthToken(ilCmiXapiAuthToken $authToken)
static getInstance(int $a_id=0, bool $a_reference=true)
if(! $DIC->user() ->getId()||!ilLTIConsumerAccess::hasCustomProviderCreationAccess()) $params
Definition: ltiregstart.php:31
catch(\Exception $e) $req
Definition: xapiproxy.php:91
$GLOBALS["DIC"]
Definition: wac.php:54