ILIAS  trunk Revision v12.0_alpha-377-g3641b37b9db
class.ilXapiStatementEvaluation.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
31{
36 protected array $resultStatusByXapiVerbMap = [
37 ilCmiXapiVerbList::COMPLETED => "completed",
38 ilCmiXapiVerbList::PASSED => "passed",
39 ilCmiXapiVerbList::FAILED => "failed",
41 ];
42
43 protected array $resultProgressByXapiVerbMap = [
44 ilCmiXapiVerbList::PROGRESSED => "progressed",
45 ilCmiXapiVerbList::EXPERIENCED => "experienced"
46 ];
47
48 protected ilObject $object;
49
50 //todo
51 protected ilLogger $log;
52
53 protected ?int $lpMode;
54
59 {
60 $this->log = $log;
61 $this->object = $object;
62
63 $objLP = ilObjectLP::getInstance($this->object->getId());
64 $this->lpMode = $objLP->getCurrentMode();
65 }
66
67 public function evaluateReport(ilCmiXapiStatementsReport $report): void
68 {
69 foreach ($report->getStatements() as $xapiStatement) {
70 #$this->log->debug(
71 # "handle statement:\n".json_encode($xapiStatement, JSON_PRETTY_PRINT)
72 #);
73
74 // ensure json decoded non assoc
75 $xapiStatement = json_decode(json_encode($xapiStatement));
76 $cmixUser = $this->getCmixUser($xapiStatement);
77
78 if ($cmixUser != null) {
79 $this->evaluateStatement($xapiStatement, $cmixUser->getUsrId());
80
81 $this->log->debug('update lp for object (' . $this->object->getId() . ')');
82 if ($cmixUser->getUsrId() != ANONYMOUS_USER_ID) {
83 ilLPStatusWrapper::_updateStatus($this->object->getId(), $cmixUser->getUsrId());
84 }
85 }
86 }
87 }
88
89 public function getCmixUser(object $xapiStatement): ?\ilCmiXapiUser
90 {
91 $cmixUser = null;
92 if ($this->object->getContentType() == ilObjCmiXapi::CONT_TYPE_CMI5) {
93 if (isset($xapiStatement->actor->account->name)) {
95 $this->object->getId(),
96 $xapiStatement->actor->account->name
97 );
98 }
99 } else {
101 $this->object->getId(),
102 str_replace('mailto:', '', $xapiStatement->actor->mbox)
103 );
104 }
105 return $cmixUser;
106 }
107
108 public function evaluateStatement(object $xapiStatement, int $usrId): void
109 {
110 global $DIC;
111 $xapiVerb = $this->getXapiVerb($xapiStatement);
112
113 if ($this->isValidXapiStatement($xapiStatement)) {
114 // result status and if exists scaled score
115 if ($this->hasResultStatusRelevantXapiVerb($xapiVerb)) {
116 if (!$this->isValidObject($xapiStatement)) {
117 return;
118 }
119 $userResult = $this->getUserResult($usrId);
120
121 $oldResultStatus = $userResult->getStatus();
122 $newResultStatus = $this->getResultStatusForXapiVerb($xapiVerb);
123
124 // this is for both xapi and cmi5
125 if ($this->isResultStatusToBeReplaced($oldResultStatus, $newResultStatus)) {
126 $this->log->debug("isResultStatusToBeReplaced: true");
127 $userResult->setStatus($newResultStatus);
128 }
129
130 if ($this->hasXapiScore($xapiStatement)) {
131 $xapiScore = $this->getXapiScore($xapiStatement);
132 $this->log->debug("Score: " . $xapiScore);
133 $userResult->setScore((float) $xapiScore);
134 }
135 $userResult->save();
136
137 // only cmi5
138 if ($this->object->getContentType() == ilObjCmiXapi::CONT_TYPE_CMI5) {
139 if (($xapiVerb == ilCmiXapiVerbList::COMPLETED || $xapiVerb == ilCmiXapiVerbList::PASSED) && $this->isLpModeInterestedInResultStatus($newResultStatus, false)) {
140 // it is possible to check against authToken usrId!
141 $cmixUser = $this->getCmixUser($xapiStatement);
142 if ($cmixUser != null) {
143 $cmixUser->setSatisfied(true);
144 $cmixUser->save();
145 $this->sendSatisfiedStatement($cmixUser);
146 }
147 }
148 }
149 }
150 // result progress (i think only cmi5 relevant)
151 if ($this->hasResultProgressRelevantXapiVerb($xapiVerb)) {
152 $userResult = $this->getUserResult($usrId);
153 $progressedScore = $this->getProgressedScore($xapiStatement);
154 if ($progressedScore !== null && $progressedScore > 0) {
155 $userResult->setScore((float) ($progressedScore / 100));
156 $userResult->save();
157 }
158 }
159 }
160 }
161
162 protected function isValidXapiStatement(object $xapiStatement): bool
163 {
164 if (!isset($xapiStatement->actor)) {
165 return false;
166 }
167
168 if (!isset($xapiStatement->verb) || !isset($xapiStatement->verb->id)) {
169 return false;
170 }
171
172 if (!isset($xapiStatement->object) || !isset($xapiStatement->object->id)) {
173 return false;
174 }
175
176 return true;
177 }
178
182 protected function isValidObject(object $xapiStatement): bool
183 {
184 if ($xapiStatement->object->id != $this->object->getActivityId()) {
185 $this->log->debug($xapiStatement->object->id . " != " . $this->object->getActivityId());
186 return false;
187 }
188 return true;
189 }
190
191
192 protected function getXapiVerb(object $xapiStatement): string
193 {
194 return $xapiStatement->verb->id;
195 }
196
197 protected function getResultStatusForXapiVerb(string $xapiVerb): string
198 {
199 return $this->resultStatusByXapiVerbMap[$xapiVerb];
200 }
201
202 protected function hasResultStatusRelevantXapiVerb(string $xapiVerb): bool
203 {
204 return isset($this->resultStatusByXapiVerbMap[$xapiVerb]);
205 }
206
207 protected function getResultProgressForXapiVerb(string $xapiVerb)
208 {
209 return $this->resultProgressByXapiVerbMap[$xapiVerb];
210 }
211
212 protected function hasResultProgressRelevantXapiVerb(string $xapiVerb): bool
213 {
214 return isset($this->resultProgressByXapiVerbMap[$xapiVerb]);
215 }
216
217 protected function hasXapiScore(object $xapiStatement): bool
218 {
219 if (!isset($xapiStatement->result)) {
220 return false;
221 }
222
223 if (!isset($xapiStatement->result->score)) {
224 return false;
225 }
226
227 if (!isset($xapiStatement->result->score->scaled)) {
228 return false;
229 }
230
231 return true;
232 }
233
234 protected function getXapiScore(object $xapiStatement)
235 {
236 return $xapiStatement->result->score->scaled;
237 }
238
239 protected function getProgressedScore(object $xapiStatement): ?float
240 {
241 if (!isset($xapiStatement->result)) {
242 return null;
243 }
244
245 if (!isset($xapiStatement->result->extensions)) {
246 return null;
247 }
248
249 if (!isset($xapiStatement->result->extensions->{'https://w3id.org/xapi/cmi5/result/extensions/progress'})) {
250 return null;
251 }
252 return (float) $xapiStatement->result->extensions->{'https://w3id.org/xapi/cmi5/result/extensions/progress'};
253 }
254
255 protected function getUserResult(int $usrId): \ilCmiXapiResult
256 {
257 try {
258 $result = ilCmiXapiResult::getInstanceByObjIdAndUsrId($this->object->getId(), $usrId);
259 } catch (ilCmiXapiException $e) {
261 $result->setObjId($this->object->getId());
262 $result->setUsrId($usrId);
263 }
264
265 return $result;
266 }
267
268 protected function isResultStatusToBeReplaced(string $oldResultStatus, string $newResultStatus): bool
269 {
270 if (!$this->isLpModeInterestedInResultStatus($newResultStatus)) {
271 $this->log->debug("isLpModeInterestedInResultStatus: false");
272 return false;
273 }
274
275 if (!$this->doesNewResultStatusDominateOldOne($oldResultStatus, $newResultStatus)) {
276 $this->log->debug("doesNewResultStatusDominateOldOne: false");
277 return false;
278 }
279
280 if ($this->needsAvoidFailedEvaluation($oldResultStatus, $newResultStatus)) {
281 $this->log->debug("needsAvoidFailedEvaluation: false");
282 return false;
283 }
284
285 return true;
286 }
287
288 protected function isLpModeInterestedInResultStatus(string $resultStatus, ?bool $deactivated = true): ?bool
289 {
290 if ($this->lpMode == ilLPObjSettings::LP_MODE_DEACTIVATED) {
291 return $deactivated;
292 }
293
294 switch ($resultStatus) {
295 case 'failed':
296
297 return in_array($this->lpMode, [
301 ]);
302
303 case 'passed':
304
305 return in_array($this->lpMode, [
310 ]);
311
312 case 'completed':
313
314 return in_array($this->lpMode, [
319 ]);
320 }
321
322 return false;
323 }
324
325 protected function doesNewResultStatusDominateOldOne(string $oldResultStatus, string $newResultStatus): bool
326 {
327 if ($oldResultStatus == '') {
328 return true;
329 }
330
331 if (in_array($newResultStatus, ['passed', 'failed'])) {
332 return true;
333 }
334
335 if (!in_array($oldResultStatus, ['passed', 'failed'])) {
336 return true;
337 }
338
339 return false;
340 }
341
342 protected function needsAvoidFailedEvaluation(string $oldResultStatus, string $newResultStatus): bool
343 {
344 if (!$this->object->isKeepLpStatusEnabled()) {
345 return false;
346 }
347
348 if ($newResultStatus != 'failed') {
349 return false;
350 }
351
352 return $oldResultStatus == 'completed' || $oldResultStatus == 'passed';
353 }
354
355
356 protected function sendSatisfiedStatement(ilCmiXapiUser $cmixUser): void
357 {
358 global $DIC;
359
360 $lrsType = $this->object->getLrsType();
361 $defaultLrs = $lrsType->getLrsEndpoint();
362 $defaultBasicAuth = $lrsType->getBasicAuth();
363
364 $defaultHeaders = [
365 'X-Experience-API-Version: 1.0.3',
366 'Authorization: ' . $defaultBasicAuth,
367 'Content-Type: application/json;charset=utf-8',
368 'Cache-Control: no-cache, no-store, must-revalidate'
369 ];
370
371 $satisfiedStatement = $this->object->getSatisfiedStatement($cmixUser);
372 $satisfiedStatementParams = ['statementId' => $satisfiedStatement['id']];
373 $defaultStatementsUrl = $defaultLrs . "/statements";
374 $defaultSatisfiedStatementUrl = $defaultStatementsUrl . '?' . ilCmiXapiAbstractRequest::buildQuery($satisfiedStatementParams);
375
376 // --- cURL Init ---
377 $ch = curl_init($defaultSatisfiedStatementUrl);
378 curl_setopt_array($ch, [
379 CURLOPT_CUSTOMREQUEST => "PUT",
380 CURLOPT_HTTPHEADER => $defaultHeaders,
381 CURLOPT_POSTFIELDS => json_encode($satisfiedStatement),
382 CURLOPT_RETURNTRANSFER => true,
383 CURLOPT_CONNECTTIMEOUT => 10,
384 CURLOPT_SSL_VERIFYPEER => true,
385 ]);
386
387 $responseBody = curl_exec($ch);
388 $statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
389
390 if ($responseBody === false) {
391 $error = curl_error($ch);
392 $this->log->error('cURL error while sending satisfied statement: ' . $error);
393 } else {
394 try {
396 ['status' => $statusCode, 'body' => $responseBody],
397 $responseBody,
398 [204]
399 );
400 } catch (Exception $e) {
401 $this->log->error('checkResponse failed: ' . $e->getMessage());
402 }
403 }
404
405 curl_close($ch);
406 }
407
408}
static buildQuery(array $params, $encoding=PHP_QUERY_RFC3986)
static checkResponse(array $response, &$body, array $allowedStatus=[200, 204])
static getInstanceByObjIdAndUsrId(int $objId, int $usrId)
static getInstanceByObjectIdAndUsrIdent(int $objId, string $usrIdent)
const LP_MODE_CMIX_COMPL_OR_PASSED_WITH_FAILED
static _updateStatus(int $a_obj_id, int $a_usr_id, ?object $a_obj=null, bool $a_percentage=false, bool $a_force_raise=false)
Component logger with individual log levels by component id.
static getInstance(int $obj_id)
Class ilObject Basic functions for all objects.
sendSatisfiedStatement(ilCmiXapiUser $cmixUser)
isLpModeInterestedInResultStatus(string $resultStatus, ?bool $deactivated=true)
__construct(ilLogger $log, ilObject $object)
ilXapiStatementEvaluation constructor.
isResultStatusToBeReplaced(string $oldResultStatus, string $newResultStatus)
evaluateReport(ilCmiXapiStatementsReport $report)
evaluateStatement(object $xapiStatement, int $usrId)
doesNewResultStatusDominateOldOne(string $oldResultStatus, string $newResultStatus)
array $resultStatusByXapiVerbMap
http://adlnet.gov/expapi/verbs/satisfied: should never be sent by AU https://github....
needsAvoidFailedEvaluation(string $oldResultStatus, string $newResultStatus)
const ANONYMOUS_USER_ID
Definition: constants.php:27
global $DIC
Definition: shib_login.php:26