ILIAS  trunk Revision v12.0_alpha-377-g3641b37b9db
class.ilCmiXapiStatementsDeleteRequest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
29{
30 public const DELETE_SCOPE_FILTERED = "filtered";
31 public const DELETE_SCOPE_ALL = "all";
32 public const DELETE_SCOPE_OWN = "own";
33
34 private ?int $usrId;
35
36 private string $activityId;
37
38 protected string $scope;
39
41
42 protected int $objId;
43
45
46 protected string $endpointDefault = '';
47
48 protected string $endpointFallback = '';
49
50 protected array $headers;
51
52 protected array $defaultHeaders;
53
54 protected ilLogger $log;
55
56 public function __construct(
57 int $obj_id,
58 int $type_id,
59 string $activity_id,
60 ?int $usr_id = null,
61 ?string $scope = self::DELETE_SCOPE_FILTERED,
63 ) {
64 $this->objId = $obj_id;
65 $this->lrsType = new ilCmiXapiLrsType($type_id);
66 $this->activityId = $activity_id;
67 $this->usrId = $usr_id;
68 $this->scope = $scope;
69 $this->filter = $filter;
70
71 $this->endpointDefault = $this->lrsType->getLrsEndpoint();
72 $this->headers = [
73 'X-Experience-API-Version' => '1.0.3'
74 ];
75 $this->defaultHeaders = $this->headers;
76 $this->defaultHeaders['Authorization'] = $this->lrsType->getBasicAuth();
77
78 $this->log = ilLoggerFactory::getLogger('cmix');
79 }
80
84 public function delete(): bool
85 {
86 global $DIC;
87 $allResponses = $this->deleteData();
88 $resStatements = $allResponses['statements'];
89 $resStates = $allResponses['states'];
90 $defaultRejected = isset($resStatements['default']) && isset($resStatements['default']['state']) && $resStatements['default']['state'] === 'rejected';
91 $resArr = array();
92 // ToDo: fullfilled and status code handling
93 if (isset($resStatements['default']) && isset($resStatements['default']['value'])) {
94 $res = $resStatements['default']['value'];
95 $resBody = json_decode((string) $res->getBody(), true);
96 $resArr[] = $resBody['_id'];
97 }
98 if (count($resArr) == 0) {
99 $this->log->debug("No data deleted");
100 return !$defaultRejected;
101 }
102
103 $maxtime = 240; // should be some minutes!
104 $t = 0;
105 $done = false;
106 while ($t < $maxtime) {
107 // get batch done
108 sleep(1);
109 $response = $this->queryBatch($resArr);
110 if (isset($response['default']) && isset($response['default']['value'])) {
111 $res = $response['default']['value'];
112 $resBody = json_decode((string) $res->getBody(), true);
113 if ($resBody && $resBody['edges'] && count($resBody['edges']) == 1) {
114 $doneDefault = $resBody['edges'][0]['node']['done'];
115 $this->log->debug("doneDefault: " . $doneDefault);
116 }
117 }
118 if ($doneDefault) {
119 $done = true;
120 break;
121 }
122 $t++;
123 }
124 if ($done) {
126 }
127 return $done;
128 }
129
130 public function deleteData(): array
131 {
132 global $DIC;
133
134 $deleteState = true;
135
136 $f = null;
137 if ($this->scope === self::DELETE_SCOPE_FILTERED) {
138 $deleteState = $this->checkDeleteState();
139 $f = $this->buildDeleteFiltered();
140 }
141 if ($this->scope === self::DELETE_SCOPE_ALL) {
142 $f = $this->buildDeleteAll();
143 }
144 if ($this->scope === self::DELETE_SCOPE_OWN) {
145 $f = $this->buildDeleteOwn();
146 }
147 if ($f === false) {
148 $this->log->debug('error: could not build filter');
149 return array();
150 }
151 $cf = array('filter' => $f);
152 $body = json_encode($cf);
153 $this->defaultHeaders['Content-Type'] = 'application/json; charset=utf-8';
154 $defaultUrl = $this->lrsType->getLrsEndpointDeleteLink();
155 $promisesStatements = [
156 'default' => $this->sendCurlRequest('POST', $defaultUrl, $this->defaultHeaders, $body),
157 ];
158 $promisesStates = array();
159 if ($deleteState) {
160 $urls = $this->getDeleteStateUrls($this->lrsType->getLrsEndpointStateLink());
161 foreach ($urls as $i => $v) {
162 $promisesStates['default' . $i] = $this->sendCurlRequest('DELETE', $v, $this->defaultHeaders, $body);
163 }
164 }
165 $response = array();
166 $response['statements'] = array();
167 $response['states'] = array();
168
169 try { // maybe everything into one promise?
170 $response['statements'] = $this->executeMultiCurl($promisesStatements);
171 if ($deleteState && count($promisesStates) > 0) {
172 $response['states'] = $this->executeMultiCurl($promisesStates);
173 }
174 } catch (Exception $e) {
175 $this->log->debug('error:' . $e->getMessage());
176 }
177 return $response;
178 }
179
180 public function _lookUpDataCount($scope = null)
181 {
182 global $DIC;
183 $pipeline = array();
184 if (is_null($scope)) {
186 }
187 if ($scope === self::DELETE_SCOPE_OWN) {
188 $f = $this->buildDeleteOwn();
189 if (count($f) == 0) {
190 return 0;
191 }
192 }
193 if ($scope === self::DELETE_SCOPE_FILTERED) {
194 $f = $this->buildDeleteFiltered();
195 }
196 if ($scope === self::DELETE_SCOPE_ALL) {
197 $f = $this->buildDeleteAll();
198 }
199 $pipeline[] = array('$match' => $f);
200 $pipeline[] = array('$count' => 'count');
201 $pquery = urlencode(json_encode($pipeline));
202 $query = "pipeline={$pquery}";
203 $purl = $this->lrsType->getLrsEndpointStatementsAggregationLink();
204 $url = ilUtil::appendUrlParameterString($purl, $query);
205 try {
206 $response = $this->sendCurlRequest('GET', $url, $this->defaultHeaders);
207 if ($response['status'] === 200) {
208 $cnt = json_decode($response['body'], true);
209 }
210 return (int) $cnt[0]->count;
211 } catch (Exception $e) {
212 throw new Exception("LRS Connection Problems");
213 return 0;
214 }
215 }
216
217 public function queryBatch(array $batchId): array
218 {
219 global $DIC;
220 $defaultUrl = $this->getBatchUrl($this->lrsType->getLrsEndpointBatchLink(), $batchId[0]);
221
222 // Header formatieren
223 $headers = [];
224 foreach ($this->defaultHeaders as $key => $value) {
225 $headers[] = "$key: $value";
226 }
227
228 $ch = curl_init($defaultUrl);
229 curl_setopt_array($ch, [
230 CURLOPT_RETURNTRANSFER => true,
231 CURLOPT_HTTPHEADER => $headers,
232 CURLOPT_TIMEOUT => 30,
233 CURLOPT_SSL_VERIFYPEER => true,
234 CURLOPT_FOLLOWLOCATION => true,
235 ]);
236
237 $response = [];
238 try {
239 $body = curl_exec($ch);
240 $error = curl_error($ch);
241 $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
242 curl_close($ch);
243
244 if ($error) {
245 throw new Exception("cURL error: $error");
246 }
247
248 $response['default'] = [
249 'state' => ($httpCode >= 200 && $httpCode < 300) ? 'fulfilled' : 'rejected',
250 'value' => (object) [
251 'status' => $httpCode,
252 'body' => $body
253 ]
254 ];
255
256 } catch (Exception $e) {
257 $this->log->debug('error:' . $e->getMessage());
258 $response['default'] = [
259 'state' => 'rejected',
260 'reason' => $e->getMessage()
261 ];
262 }
263
264 return $response;
265 }
266
267 private function getBatchUrl(string $url, string $batchId): string
268 {
269 $f = array();
270 $f['_id'] = [
271 '$oid' => $batchId
272 ];
273 $f = urlencode(json_encode($f));
274 $f = "filter={$f}";
276 }
277
278 private function getDeleteStateUrls($url): array
279 {
280 $ret = array();
281 $states = $this->buildDeleteStates();
282 foreach ($states as $i => $v) {
284 }
285 return $ret;
286 }
287
288 private function buildDeleteAll(): array
289 {
290 global $DIC;
291 $f = array();
292
293 $f['statement.object.objectType'] = 'Activity';
294 $f['statement.object.id'] = [
295 '$regex' => '^' . preg_quote($this->activityId) . ''
296 ];
297
298 $f['statement.actor.objectType'] = 'Agent';
299
300 $f['$or'] = [];
301 // foreach (ilXapiCmi5User::getUsersForObjectPlugin($this->getObjId()) as $usr_id) {
302 // $f['$or'][] = ['statement.actor.mbox' => "mailto:".ilXapiCmi5User::getUsrIdentPlugin($usr_id,$this->getObjId())];
303 foreach (ilCmiXapiUser::getUsersForObject($this->objId) as $cmixUser) {
304 $f['$or'][] = ['statement.actor.mbox' => "mailto:{$cmixUser->getUsrIdent()}"];
305 }
306 if (count($f['$or']) == 0) {
307 // Exception Handling!
308 return [];
309 } else {
310 return $f;
311 }
312 }
313
314 private function buildDeleteFiltered(): array
315 {
316 global $DIC;
317 $f = array();
318
319 $f['statement.object.objectType'] = 'Activity';
320 $f['statement.object.id'] = [
321 '$regex' => '^' . preg_quote($this->activityId) . ''
322 ];
323
324 $f['statement.actor.objectType'] = 'Agent';
325 $f['$or'] = [];
326 if ($this->filter->getActor()) {
327 foreach (ilCmiXapiUser::getUsersForObject($this->objId) as $cmixUser) {
328 if ($cmixUser->getUsrId() == $this->filter->getActor()->getUsrId()) {
329 $f['$or'][] = ['statement.actor.mbox' => "mailto:{$cmixUser->getUsrIdent()}"];
330 }
331 }
332 } else { // check hasOutcomes Access?
333 foreach (ilCmiXapiUser::getUsersForObject($this->objId) as $cmixUser) {
334 $f['$or'][] = ['statement.actor.mbox' => "mailto:{$cmixUser->getUsrIdent()}"];
335 }
336 }
337
338 if ($this->filter->getVerb()) {
339 $f['statement.verb.id'] = $this->filter->getVerb();
340 }
341
342 if ($this->filter->getStartDate() || $this->filter->getEndDate()) {
343 $f['statement.timestamp'] = array();
344
345 if ($this->filter->getStartDate()) {
346 $f['statement.timestamp']['$gt'] = $this->filter->getStartDate()->toXapiTimestamp();
347 }
348
349 if ($this->filter->getEndDate()) {
350 $f['statement.timestamp']['$lt'] = $this->filter->getEndDate()->toXapiTimestamp();
351 }
352 }
353
354 if (count($f['$or']) == 0) {
355 // Exception Handling!
356 return [];
357 } else {
358 return $f;
359 }
360 }
361
362 private function buildDeleteOwn(): array
363 {
364 global $DIC;
365 $f = array();
366 $f['statement.object.objectType'] = 'Activity';
367 $f['statement.object.id'] = [
368 '$regex' => '^' . preg_quote($this->activityId) . ''
369 ];
370 $f['statement.actor.objectType'] = 'Agent';
371
372 $usrId = ($this->usrId !== null) ? $this->usrId : $DIC->user()->getId();
373 $cmixUsers = ilCmiXapiUser::getInstancesByObjectIdAndUsrId($this->objId, $usrId);
374 $f['$or'] = [];
375 foreach ($cmixUsers as $cmixUser) {
376 $f['$or'][] = ['statement.actor.mbox' => "mailto:{$cmixUser->getUsrIdent()}"];
377 }
378 if (count($f['$or']) == 0) {
379 return [];
380 } else {
381 return $f;
382 }
383 }
384
385 private function buildDeleteStates(): array
386 {
387 global $DIC;
388 $ret = array();
389 $user = "";
390 if ($this->scope === self::DELETE_SCOPE_FILTERED && $this->filter->getActor()) {
391 foreach (ilCmiXapiUser::getUsersForObject($this->objId) as $cmixUser) {
392 if ($cmixUser->getUsrId() == $this->filter->getActor()->getUsrId()) {
393 $user = $cmixUser->getUsrIdent();
394 $ret[] = 'activityId=' . urlencode($this->activityId) . '&agent=' . urlencode('{"mbox":"mailto:' . $user . '"}');
395 }
396 }
397 }
398
399 if ($this->scope === self::DELETE_SCOPE_OWN) {
400 $usrId = ($this->usrId !== null) ? $this->usrId : $DIC->user()->getId();
401 foreach (ilCmiXapiUser::getUsersForObject($this->objId) as $cmixUser) {
402 if ((int) $cmixUser->getUsrId() === $usrId) {
403 $user = $cmixUser->getUsrIdent();
404 $ret[] = 'activityId=' . urlencode($this->activityId) . '&agent=' . urlencode('{"mbox":"mailto:' . $user . '"}');
405 }
406 }
407 }
408
409 if ($this->scope === self::DELETE_SCOPE_ALL) {
410 //todo check cmix_del_object
411 foreach (ilCmiXapiUser::getUsersForObject($this->objId) as $cmixUser) {
412 $user = $cmixUser->getUsrIdent();
413 $ret[] = 'activityId=' . urlencode($this->activityId) . '&agent=' . urlencode('{"mbox":"mailto:' . $user . '"}');
414 }
415 }
416 return $ret;
417 }
418
419 private function checkDeleteState(): bool
420 {
421 global $DIC;
422 if ($this->scope === self::DELETE_SCOPE_ALL || $this->scope === self::DELETE_SCOPE_OWN) {
423 return true;
424 }
425 if ($this->filter->getActor()) { // ToDo: only in Multicactor Mode?
426 if ($this->filter->getVerb() || $this->filter->getStartDate() || $this->filter->getEndDate()) {
427 return false;
428 } else {
429 return true;
430 }
431 }
432 return false;
433 }
434
435 private function checkDeleteUsersForObject()
436 {
437 global $DIC;
438 if ($this->scope === self::DELETE_SCOPE_ALL) {
440 // $model = ilCmiXapiDelModel::init();
441 // $model->deleteXapiObjectEntry($this->objId);
442 }
443 if ($this->scope === self::DELETE_SCOPE_OWN) {
444 $usrId = ($this->usrId !== null) ? [$this->usrId] : [$DIC->user()->getId()];
446 }
447 if ($this->scope === self::DELETE_SCOPE_FILTERED) {
448 if ($this->checkDeleteState() && $this->filter) {
449 $usrId = [$this->filter->getActor()->getUsrId()];
451 }
452 }
453 }
454 private function sendCurlRequest(string $method, string $url, array $headers = [], ?string $body = null): array
455 {
456 $ch = curl_init($url);
457
458 $formattedHeaders = [];
459 foreach ($headers as $key => $value) {
460 $formattedHeaders[] = "$key: $value";
461 }
462
463 curl_setopt_array($ch, [
464 CURLOPT_RETURNTRANSFER => true,
465 CURLOPT_CUSTOMREQUEST => $method,
466 CURLOPT_HTTPHEADER => $formattedHeaders,
467 CURLOPT_CONNECTTIMEOUT => 10,
468 CURLOPT_TIMEOUT => 60,
469 CURLOPT_SSL_VERIFYPEER => true,
470 ]);
471
472 if ($body !== null && in_array($method, ['POST', 'PUT', 'PATCH'])) {
473 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
474 }
475
476 $responseBody = curl_exec($ch);
477 $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
478 $error = curl_error($ch);
479
480 curl_close($ch);
481
482 return [
483 'status' => $statusCode,
484 'body' => $responseBody,
485 'error' => $error ?: null,
486 ];
487 }
488 private function executeMultiCurl(array $requests): array
489 {
490 $mh = curl_multi_init();
491 $handles = [];
492
493 // cURL Handles vorbereiten
494 foreach ($requests as $key => $req) {
495 if (!is_array($req)) {
496 // Falls $req direkt URL + Method enthält (z. B. ['method' => 'DELETE', 'url' => '...'])
497 $method = $req['method'] ?? 'GET';
498 $url = $req['url'] ?? '';
499 $headers = $req['headers'] ?? $this->defaultHeaders;
500 $body = $req['body'] ?? null;
501 } else {
502 // wenn wir direkt die Parameter übergeben
503 $method = $req['method'] ?? 'GET';
504 $url = $req['url'] ?? '';
505 $headers = $req['headers'] ?? $this->defaultHeaders;
506 $body = $req['body'] ?? null;
507 }
508
509 $ch = curl_init($url);
510 $formattedHeaders = [];
511 foreach ($headers as $k => $v) {
512 $formattedHeaders[] = "$k: $v";
513 }
514
515 curl_setopt_array($ch, [
516 CURLOPT_RETURNTRANSFER => true,
517 CURLOPT_CUSTOMREQUEST => $method,
518 CURLOPT_HTTPHEADER => $formattedHeaders,
519 CURLOPT_CONNECTTIMEOUT => 10,
520 CURLOPT_TIMEOUT => 60,
521 CURLOPT_SSL_VERIFYPEER => true
522 ]);
523
524 if ($body !== null && in_array($method, ['POST', 'PUT', 'PATCH'])) {
525 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
526 }
527
528 $handles[$key] = $ch;
529 curl_multi_add_handle($mh, $ch);
530 }
531
532 // Alle gleichzeitig ausführen
533 $running = 0;
534 do {
535 curl_multi_exec($mh, $running);
536 curl_multi_select($mh);
537 } while ($running > 0);
538
539 // Ergebnisse einsammeln
540 $responses = [];
541 foreach ($handles as $key => $ch) {
542 $responses[$key] = [
543 'state' => 'fulfilled',
544 'value' => (object) [
545 'status' => curl_getinfo($ch, CURLINFO_HTTP_CODE),
546 'body' => curl_multi_getcontent($ch),
547 'error' => curl_error($ch) ?: null
548 ]
549 ];
550
551 curl_multi_remove_handle($mh, $ch);
552 curl_close($ch);
553 }
554
555 curl_multi_close($mh);
556
557 return $responses;
558 }
559
560}
__construct(int $obj_id, int $type_id, string $activity_id, ?int $usr_id=null, ?string $scope=self::DELETE_SCOPE_FILTERED, ?ilCmiXapiStatementsReportFilter $filter=null)
sendCurlRequest(string $method, string $url, array $headers=[], ?string $body=null)
static deleteUsersForObject(int $objId, ?array $users=[])
static getInstancesByObjectIdAndUsrId(int $objId, int $usrId)
static getUsersForObject(int $objId, bool $asUsrId=false)
static getLogger(string $a_component_id)
Get component logger.
Component logger with individual log levels by component id.
static appendUrlParameterString(string $a_url, string $a_par, bool $xml_style=false)
if(empty($ltiMessageHint)) $mh
Definition: ltiauth.php:64
$res
Definition: ltiservices.php:69
filter(string $filter_id, array $class_path, string $cmd, bool $activated=true, bool $expanded=true)
catch(\Exception $e) $req
Definition: xapiproxy.php:78
global $DIC
Definition: shib_login.php:26
$url
Definition: shib_logout.php:68
$response
Definition: xapitoken.php:90