ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
class.ilCmiXapiLaunchGUI.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
31{
32 public const XAPI_PROXY_ENDPOINT = 'components/ILIAS/CmiXapi/xapiproxy.php';
33
35
37
38 protected bool $plugin = false;
39
41
43
45 {
46 $this->object = $object;
47 }
48
49 public function executeCommand(): void
50 {
51 global $DIC;
52 $this->user = $DIC->user();
53 $this->ctrl = $DIC->ctrl();
54 $this->launchCmd();
55 }
56
57 protected function launchCmd(): void
58 {
59 $this->initCmixUser();
60 $token = $this->getValidToken();
61 if ($this->object->getContentType() == ilObjCmiXapi::CONT_TYPE_CMI5) {
62 $ret = $this->CMI5preLaunch($token);
63 $token = $ret['token'];
64 }
65 $launchLink = $this->buildLaunchLink($token);
66 $this->ctrl->redirectToURL($launchLink);
67 }
68
69 protected function buildLaunchLink(string $token): string
70 {
71 $launchLink = "";
72
73 if ($this->object->getSourceType() == ilObjCmiXapi::SRC_TYPE_REMOTE) {
74 $launchLink = $this->object->getLaunchUrl();
75 } elseif ($this->object->getSourceType() == ilObjCmiXapi::SRC_TYPE_LOCAL) {
76 if (preg_match("/^(https?:\/\/)/", $this->object->getLaunchUrl()) == 1) {
77 $launchLink = $this->object->getLaunchUrl();
78 } else {
79 $launchLink = implode('/', [
80 ILIAS_HTTP_PATH,
83 ]);
84
85 $launchLink .= DIRECTORY_SEPARATOR . $this->object->getLaunchUrl();
86 }
87 }
88
89 foreach ($this->getLaunchParameters($token) as $paramName => $paramValue) {
90 $launchLink = ilUtil::appendUrlParameterString($launchLink, "{$paramName}={$paramValue}");
91 }
92
93 return $launchLink;
94 }
95
99 protected function getLaunchParameters(string $token): array
100 {
101 $params = [];
102
103 if ($this->object->isBypassProxyEnabled()) {
104 $params['endpoint'] = urlencode(rtrim($this->object->getLrsType()->getLrsEndpoint(), '/') . '/');
105 } else {
106 $params['endpoint'] = urlencode(rtrim(ILIAS_HTTP_PATH . '/' . self::XAPI_PROXY_ENDPOINT, '/') . '/');
107 }
108
109 if ($this->object->isAuthFetchUrlEnabled()) {
110 $params['fetch'] = urlencode($this->getAuthTokenFetchLink());
111 } else {
112 if ($this->object->isBypassProxyEnabled()) {
113 $params['auth'] = urlencode($this->object->getLrsType()->getBasicAuth());
114 } else {
115 $params['auth'] = urlencode('Basic ' . base64_encode(
116 CLIENT_ID . ':' . $token
117 ));
118 }
119 }
120
121 $params['activity_id'] = urlencode($this->object->getActivityId());
122 $params['activityId'] = urlencode($this->object->getActivityId());
123 $params['actor'] = urlencode(json_encode($this->object->getStatementActor($this->cmixUser)));
124 if ($this->object->getContentType() == ilObjCmiXapi::CONT_TYPE_CMI5) {
125 $registration = $this->cmixUser->getRegistration();
126 // for old CMI5 Content after switch commit but before cmi5 bugfix
127 if ($registration == '') {
128 $registration = ilCmiXapiUser::generateRegistration($this->object, $this->user);
129 }
130 $params['registration'] = $registration;
131 } else {
132 $params['registration'] = urlencode((string) ilCmiXapiUser::generateRegistration($this->object, $this->user));
133 }
134 return $params;
135 }
136
140 protected function getAuthTokenFetchLink(): string
141 {
142 $link = implode('/', [
143 ILIAS_HTTP_PATH, 'components/ILIAS', 'CmiXapi', 'xapitoken.php'
144 ]);
145
147
148 return iLUtil::appendUrlParameterString($link, "param={$param}");
149 }
150
154 protected function buildAuthTokenFetchParam(): string
155 {
156 $params = [
157 session_name() => session_id(),
158 'obj_id' => $this->object->getId(),
159 'ref_id' => $this->object->getRefId(),
160 'ilClientId' => CLIENT_ID
161 ];
162
163 $encryptionKey = ilCmiXapiAuthToken::getWacSalt();
164 return urlencode(base64_encode(openssl_encrypt(
165 json_encode($params),
167 $encryptionKey,
168 0,
170 )));
171 }
172
173 protected function getValidToken(): string
174 {
176 $this->user->getId(),
177 $this->object->getRefId(),
178 $this->object->getId(),
179 $this->object->getLrsType()->getTypeId()
180 );
181 return $token;
182 }
183
184 protected function initCmixUser(): void
185 {
186 $this->cmixUser = new ilCmiXapiUser($this->object->getId(), $this->user->getId(), $this->object->getPrivacyIdent());
187 $user_ident = $this->cmixUser->getUsrIdent();
188 if ($user_ident == '' || $user_ident == null) {
189 $user_ident = ilCmiXapiUser::getIdent($this->object->getPrivacyIdent(), $this->user);
190 $this->cmixUser->setUsrIdent($user_ident);
191
192 if ($this->object->getContentType() == ilObjCmiXapi::CONT_TYPE_CMI5) {
193 $this->cmixUser->setRegistration((string) ilCmiXapiUser::generateCMI5Registration($this->object->getId(), $this->user->getId()));
194 }
195 $this->cmixUser->save();
196 if (!ilObjUser::_isAnonymous($this->user->getId())) {
197 ilLPStatusWrapper::_updateStatus($this->object->getId(), $this->user->getId());
198 }
199 }
200 }
201
205 protected function getCmi5LearnerPreferences(): array
206 {
207 $language = $this->user->getLanguage();
208 $audio = "on";
209 return [
210 "languagePreference" => "{$language}",
211 "audioPreference" => "{$audio}"
212 ];
213 }
214
223 protected function CMI5preLaunch(string $token): array
224 {
225 $duration = '';
226 $lrsType = $this->object->getLrsType();
227 $defaultLrs = $lrsType->getLrsEndpoint();
228 //$fallbackLrs = $lrsType->getLrsFallbackEndpoint();
229 $defaultBasicAuth = $lrsType->getBasicAuth();
230 //$fallbackBasicAuth = $lrsType->getFallbackBasicAuth();
231 $defaultHeaders = [
232 'X-Experience-API-Version' => '1.0.3',
233 'Authorization' => $defaultBasicAuth,
234 'Content-Type' => 'application/json;charset=utf-8',
235 'Cache-Control' => 'no-cache, no-store, must-revalidate'
236 ];
237
238 $registration = $this->cmixUser->getRegistration();
239 // for old CMI5 Content after switch commit but before cmi5 bugfix
240 if ($registration == '') {
241 $registration = ilCmiXapiUser::generateRegistration($this->object, $this->user);
242 }
243
244 $activityId = $this->object->getActivityId();
245
246 // profile
247 $profileParams = [];
248 $defaultAgentProfileUrl = $defaultLrs . "/agents/profile";
249 $profileParams['agent'] = json_encode($this->object->getStatementActor($this->cmixUser));
250 $profileParams['profileId'] = 'cmi5LearnerPreferences';
251 $defaultProfileUrl = $defaultAgentProfileUrl . '?' . ilCmiXapiAbstractRequest::buildQuery($profileParams);
252
253 // launchData
254 $launchDataParams = [];
255 $defaultStateUrl = $defaultLrs . "/activities/state";
256 //$launchDataParams['agent'] = $this->buildCmi5ActorParameter();
257 $launchDataParams['agent'] = json_encode($this->object->getStatementActor($this->cmixUser));
258 $launchDataParams['activityId'] = $activityId;
259 $launchDataParams['activity_id'] = $activityId;
260 $launchDataParams['registration'] = $registration;
261 $launchDataParams['stateId'] = 'LMS.LaunchData';
262 $defaultLaunchDataUrl = $defaultStateUrl . '?' . ilCmiXapiAbstractRequest::buildQuery($launchDataParams);
263 $cmi5LearnerPreferencesObj = $this->getCmi5LearnerPreferences();
264 $cmi5LearnerPreferences = json_encode($cmi5LearnerPreferencesObj);
265 $lang = $cmi5LearnerPreferencesObj['languagePreference'];
266 $cmi5_session = ilObjCmiXapi::guidv4();
268 $oldSession = $tokenObject->getCmi5Session();
269 $oldSessionLaunchedTimestamp = '';
270 $abandoned = false;
271 // cmi5_session already exists?
272 if ($oldSession != null && !empty($oldSession)) {
273 $oldSessionData = json_decode($tokenObject->getCmi5SessionData());
274 $oldSessionLaunchedTimestamp = $oldSessionData->launchedTimestamp;
275 $tokenObject->delete();
276 $token = $this->getValidToken();
278 $lastStatement = $this->object->getLastStatement($oldSession);
279 // should never be 'terminated', because terminated statement is sniffed from proxy -> token delete
280 if (isset($lastStatement[0]['statement']['verb']['id']) && $lastStatement[0]['statement']['verb']['id'] != ilCmiXapiVerbList::getInstance()->getVerbUri('terminated')) {
281 $abandoned = true;
282 $start = new DateTime($oldSessionLaunchedTimestamp);
283 $end = new DateTime($lastStatement[0]['statement']['timestamp']);
284 $diff = $end->diff($start);
286 }
287 }
288 // satisfied on launch?
289 // see: https://github.com/AICC/CMI-5_Spec_Current/blob/quartz/cmi5_spec.md#moveon
290 // https://aicc.github.io/CMI-5_Spec_Current/samples/
291 // Session that includes the absolute minimum data, and is associated with a NotApplicable Move On criteria
292 // which results in immediate satisfaction of the course upon registration creation. Includes Satisfied Statement.
293 $satisfied = false;
294 $lpMode = $this->object->getLPMode();
295 // only do this, if we decide to map the moveOn NotApplicable to ilLPObjSettings::LP_MODE_DEACTIVATED on import and settings editing
296 // and what about user result status?
297 if ($lpMode === ilLPObjSettings::LP_MODE_DEACTIVATED) {
298 $satisfied = true;
299 }
300
301 $tokenObject->setCmi5Session($cmi5_session);
302 $sessionData = array();
303 $sessionData['cmi5LearnerPreferences'] = $cmi5LearnerPreferencesObj;
304 //https://www.php.net/manual/de/class.dateinterval.php
305 $now = new ilCmiXapiDateTime(time(), IL_CAL_UNIX);
306 $sessionData['launchedTimestamp'] = $now->toXapiTimestamp(); // required for abandoned statement duration, don't want another roundtrip to lrs ...puhhh
307 $tokenObject->setCmi5SessionData(json_encode($sessionData));
308 $tokenObject->update();
309 $defaultStatementsUrl = $defaultLrs . "/statements";
310
311 // launchedStatement
312 $launchData = json_encode($this->object->getLaunchData($this->cmixUser, $lang));
313 $launchedStatement = $this->object->getLaunchedStatement($this->cmixUser);
314 $launchedStatementParams = [];
315 $launchedStatementParams['statementId'] = $launchedStatement['id'];
316 $defaultLaunchedStatementUrl = $defaultStatementsUrl . '?' . ilCmiXapiAbstractRequest::buildQuery($launchedStatementParams);
317
318 // abandonedStatement
319 if ($abandoned) {
320 $abandonedStatement = $this->object->getAbandonedStatement($oldSession, $duration, $this->cmixUser);
321 $abandonedStatementParams = [];
322 $abandonedStatementParams['statementId'] = $abandonedStatement['id'];
323 $defaultAbandonedStatementUrl = $defaultStatementsUrl . '?' . ilCmiXapiAbstractRequest::buildQuery($abandonedStatementParams);
324 }
325 // abandonedStatement
326 if ($satisfied) {
327 $satisfiedStatement = $this->object->getSatisfiedStatement($this->cmixUser);
328 $satisfiedStatementParams = [];
329 $satisfiedStatementParams['statementId'] = $satisfiedStatement['id'];
330 $defaultSatisfiedStatementUrl = $defaultStatementsUrl . '?' . ilCmiXapiAbstractRequest::buildQuery($satisfiedStatementParams);
331 }
332 $client = new GuzzleHttp\Client();
333 $req_opts = array(
334 GuzzleHttp\RequestOptions::VERIFY => true,
335 GuzzleHttp\RequestOptions::CONNECT_TIMEOUT => 10,
336 GuzzleHttp\RequestOptions::HTTP_ERRORS => false
337 );
338 $defaultProfileRequest = new GuzzleHttp\Psr7\Request(
339 'POST',
340 $defaultProfileUrl,
341 $defaultHeaders,
342 $cmi5LearnerPreferences
343 );
344 $defaultLaunchDataRequest = new GuzzleHttp\Psr7\Request(
345 'PUT',
346 $defaultLaunchDataUrl,
347 $defaultHeaders,
348 $launchData
349 );
350 $defaultLaunchedStatementRequest = new GuzzleHttp\Psr7\Request(
351 'PUT',
352 $defaultLaunchedStatementUrl,
353 $defaultHeaders,
354 json_encode($launchedStatement)
355 );
356 if ($abandoned) {
357 $defaultAbandonedStatementRequest = new GuzzleHttp\Psr7\Request(
358 'PUT',
359 $defaultAbandonedStatementUrl,
360 $defaultHeaders,
361 json_encode($abandonedStatement)
362 );
363 }
364 if ($satisfied) {
365 $defaultSatisfiedStatementRequest = new GuzzleHttp\Psr7\Request(
366 'PUT',
367 $defaultSatisfiedStatementUrl,
368 $defaultHeaders,
369 json_encode($satisfiedStatement)
370 );
371 }
372 $promises = array();
373 $promises['defaultProfile'] = $client->sendAsync($defaultProfileRequest, $req_opts);
374 $promises['defaultLaunchData'] = $client->sendAsync($defaultLaunchDataRequest, $req_opts);
375 $promises['defaultLaunchedStatement'] = $client->sendAsync($defaultLaunchedStatementRequest, $req_opts);
376 if ($abandoned) {
377 $promises['defaultAbandonedStatement'] = $client->sendAsync($defaultAbandonedStatementRequest, $req_opts);
378 }
379 if ($satisfied) {
380 $promises['defaultSatisfiedStatement'] = $client->sendAsync($defaultSatisfiedStatementRequest, $req_opts);
381 }
382 try {
383 $responses = GuzzleHttp\Promise\Utils::settle($promises)->wait();
384 $body = '';
385 foreach ($responses as $response) {
387 }
388 } catch (Exception $e) {
389 $this->log()->error('error:' . $e->getMessage());
390 }
391 return array('cmi5_session' => $cmi5_session, 'token' => $token);
392 }
393
397 private function log(): ilLogger
398 {
399 if ($this->plugin) {
400 global $log;
401 return $log;
402 } else {
403 return \ilLoggerFactory::getLogger('cmix');
404 }
405 }
406}
$duration
const IL_CAL_UNIX
static buildQuery(array $params, $encoding=PHP_QUERY_RFC3986)
static checkResponse(array $response, &$body, array $allowedStatus=[200, 204])
static fillToken(int $usrId, int $refId, int $objId, int $lrsTypeId=0)
static getInstanceByToken(string $token)
static dateIntervalToISO860Duration(\DateInterval $d)
__construct(ilObjCmiXapi $object)
getLaunchParameters(string $token)
CMI5preLaunch(string $token)
Prelaunch post cmi5LearnerPreference (agent profile) post LMS.LaunchData.
static getIdent(int $userIdentMode, ilObjUser $user)
static generateRegistration(ilObjCmiXapi $obj, ilObjUser $user)
static generateCMI5Registration(int $objId, int $usrId)
static getWebspaceDir(string $mode="filesystem")
get webspace directory
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 guidv4(?string $data=null)
User class.
static _isAnonymous(int $usr_id)
static appendUrlParameterString(string $a_url, string $a_par, bool $xml_style=false)
const CLIENT_ID
Definition: constants.php:41
$client
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
if(! $DIC->user() ->getId()||!ilLTIConsumerAccess::hasCustomProviderCreationAccess()) $params
Definition: ltiregstart.php:31
$log
Definition: ltiresult.php:34
global $DIC
Definition: shib_login.php:26
$lang
Definition: xapiexit.php:25
$token
Definition: xapitoken.php:70
$param
Definition: xapitoken.php:46
$response
Definition: xapitoken.php:93