ILIAS  trunk Revision v12.0_alpha-377-g3641b37b9db
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 = '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 = ILIAS_HTTP_PATH . '/xapitoken.php';
143
145
146 return iLUtil::appendUrlParameterString($link, "param={$param}");
147 }
148
152 protected function buildAuthTokenFetchParam(): string
153 {
154 $params = [
155 session_name() => session_id(),
156 'obj_id' => $this->object->getId(),
157 'ref_id' => $this->object->getRefId(),
158 'ilClientId' => CLIENT_ID
159 ];
160
161 $encryptionKey = ilCmiXapiAuthToken::getWacSalt();
162 return urlencode(base64_encode(openssl_encrypt(
163 json_encode($params),
165 $encryptionKey,
166 0,
168 )));
169 }
170
171 protected function getValidToken(): string
172 {
174 $this->user->getId(),
175 $this->object->getRefId(),
176 $this->object->getId(),
177 $this->object->getLrsType()->getTypeId()
178 );
179 return $token;
180 }
181
182 protected function initCmixUser(): void
183 {
184 $this->cmixUser = new ilCmiXapiUser($this->object->getId(), $this->user->getId(), $this->object->getPrivacyIdent());
185 $user_ident = $this->cmixUser->getUsrIdent();
186 if ($user_ident == '' || $user_ident == null) {
187 $user_ident = ilCmiXapiUser::getIdent($this->object->getPrivacyIdent(), $this->user);
188 $this->cmixUser->setUsrIdent($user_ident);
189
190 if ($this->object->getContentType() == ilObjCmiXapi::CONT_TYPE_CMI5) {
191 $this->cmixUser->setRegistration((string) ilCmiXapiUser::generateCMI5Registration($this->object->getId(), $this->user->getId()));
192 }
193 $this->cmixUser->save();
194 if (!ilObjUser::_isAnonymous($this->user->getId())) {
195 ilLPStatusWrapper::_updateStatus($this->object->getId(), $this->user->getId());
196 }
197 }
198 }
199
203 protected function getCmi5LearnerPreferences(): array
204 {
205 $language = $this->user->getLanguage();
206 $audio = "on";
207 return [
208 "languagePreference" => "{$language}",
209 "audioPreference" => "{$audio}"
210 ];
211 }
212
221 protected function CMI5preLaunch(string $token): array
222 {
223 global $DIC;
224 $DIC->language()->loadLanguageModule("cmix");
225
226 $duration = '';
227 $lrsType = $this->object->getLrsType();
228 $defaultLrs = $lrsType->getLrsEndpoint();
229 $defaultBasicAuth = $lrsType->getBasicAuth();
230
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 if ($registration == '') {
240 $registration = ilCmiXapiUser::generateRegistration($this->object, $this->user);
241 }
242
243 $activityId = $this->object->getActivityId();
244
245 // Profile URL
246 $profileParams = [
247 'agent' => json_encode($this->object->getStatementActor($this->cmixUser)),
248 'profileId' => 'cmi5LearnerPreferences'
249 ];
250 $defaultProfileUrl = $defaultLrs . "/agents/profile?" . ilCmiXapiAbstractRequest::buildQuery($profileParams);
251
252 // LaunchData URL
253 $launchDataParams = [
254 'agent' => json_encode($this->object->getStatementActor($this->cmixUser)),
255 'activityId' => $activityId,
256 'activity_id' => $activityId,
257 'registration' => $registration,
258 'stateId' => 'LMS.LaunchData'
259 ];
260 $defaultLaunchDataUrl = $defaultLrs . "/activities/state?" . ilCmiXapiAbstractRequest::buildQuery($launchDataParams);
261
262 $cmi5LearnerPreferencesObj = $this->getCmi5LearnerPreferences();
263 $cmi5LearnerPreferences = json_encode($cmi5LearnerPreferencesObj);
264 $lang = $cmi5LearnerPreferencesObj['languagePreference'];
265 $cmi5_session = ilObjCmiXapi::guidv4();
266
268 $oldSession = $tokenObject->getCmi5Session();
269 $oldSessionLaunchedTimestamp = '';
270 $abandoned = false;
271
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 if (isset($lastStatement[0]['statement']['verb']['id']) &&
280 $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
289 $satisfied = false;
290 $lpMode = $this->object->getLPMode();
291 if ($lpMode === ilLPObjSettings::LP_MODE_DEACTIVATED) {
292 $satisfied = true;
293 }
294
295 $tokenObject->setCmi5Session($cmi5_session);
296 $now = new ilCmiXapiDateTime(time(), IL_CAL_UNIX);
297 $sessionData = [
298 'cmi5LearnerPreferences' => $cmi5LearnerPreferencesObj,
299 'launchedTimestamp' => $now->toXapiTimestamp()
300 ];
301 $tokenObject->setCmi5SessionData(json_encode($sessionData));
302 $tokenObject->update();
303
304 $defaultStatementsUrl = $defaultLrs . "/statements";
305
306 // Statements
307 $launchData = json_encode($this->object->getLaunchData($DIC->language()->txt('cmiexit'), $this->cmixUser));
308 $launchedStatement = $this->object->getLaunchedStatement($this->cmixUser);
309 $launchedStatementUrl = $defaultStatementsUrl . '?statementId=' . urlencode($launchedStatement['id']);
310
311 $requests = [
312 ['url' => $defaultProfileUrl, 'method' => 'POST', 'body' => $cmi5LearnerPreferences],
313 ['url' => $defaultLaunchDataUrl, 'method' => 'PUT', 'body' => $launchData],
314 ['url' => $launchedStatementUrl, 'method' => 'PUT', 'body' => json_encode($launchedStatement)]
315 ];
316
317 if ($abandoned) {
318 $abandonedStatement = $this->object->getAbandonedStatement($oldSession, $duration, $this->cmixUser);
319 $requests[] = [
320 'url' => $defaultStatementsUrl . '?statementId=' . urlencode($abandonedStatement['id']),
321 'method' => 'PUT',
322 'body' => json_encode($abandonedStatement)
323 ];
324 }
325
326 if ($satisfied) {
327 $satisfiedStatement = $this->object->getSatisfiedStatement($this->cmixUser);
328 $requests[] = [
329 'url' => $defaultStatementsUrl . '?statementId=' . urlencode($satisfiedStatement['id']),
330 'method' => 'PUT',
331 'body' => json_encode($satisfiedStatement)
332 ];
333 }
334
335 // --- Native cURL Multi ---
336 $mh = curl_multi_init();
337 $chs = [];
338
339 foreach ($requests as $req) {
340 $ch = curl_init();
341 curl_setopt_array($ch, [
342 CURLOPT_URL => $req['url'],
343 CURLOPT_CUSTOMREQUEST => $req['method'],
344 CURLOPT_HTTPHEADER => $defaultHeaders,
345 CURLOPT_POSTFIELDS => $req['body'],
346 CURLOPT_RETURNTRANSFER => true,
347 CURLOPT_TIMEOUT => 10,
348 CURLOPT_SSL_VERIFYPEER => true,
349 ]);
350 curl_multi_add_handle($mh, $ch);
351 $chs[] = $ch;
352 }
353
354 // Execute all
355 $running = 0;
356 do {
357 curl_multi_exec($mh, $running);
358 curl_multi_select($mh);
359 // usleep(10000); // 10ms Pause, schont CPU
360 } while ($running > 0);
361
362 // Collect responses
363 foreach ($chs as $ch) {
364 $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
365 $body = curl_multi_getcontent($ch);
366 if (!in_array($status, [200, 204])) {
367 $this->log()->error("CMI5preLaunch HTTP error $status: $body");
368 }
369 curl_multi_remove_handle($mh, $ch);
370 curl_close($ch);
371 }
372
373 curl_multi_close($mh);
374
375 return [
376 'cmi5_session' => $cmi5_session,
377 'token' => $token
378 ];
379 }
380
384 private function log(): ilLogger
385 {
386 return \ilLoggerFactory::getLogger('cmix');
387 }
388}
$duration
const IL_CAL_UNIX
static buildQuery(array $params, $encoding=PHP_QUERY_RFC3986)
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
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
if(empty($ltiMessageHint)) $mh
Definition: ltiauth.php:64
if(! $DIC->user() ->getId()||!ilLTIConsumerAccess::hasCustomProviderCreationAccess()) $params
Definition: ltiregstart.php:31
catch(\Exception $e) $req
Definition: xapiproxy.php:78
global $DIC
Definition: shib_login.php:26
$token
Definition: xapitoken.php:67
$param
Definition: xapitoken.php:44