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