ILIAS  release_7 Revision v7.30-3-g800a261c036
All Data Structures Namespaces Files Functions Variables Modules Pages
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 
37  public function __construct(ilObjCmiXapi $object)
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 
138  $param = $this->buildAuthTokenFetchParam();
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) {
392  ilCmiXapiAbstractRequest::checkResponse($response, $body, [204]);
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 }
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.
if($_SERVER['argc']< 4) $client
Definition: cron.php:12
const IL_CAL_UNIX
__construct(ilObjCmiXapi $object)
static appendUrlParameterString($a_url, $a_par, $xml_style=false)
append URL parameter string ("par1=value1&par2=value2...") to given URL string
$log
Definition: result.php:15
$token
Definition: xapitoken.php:52
static generateCMI5Registration($objId, $usrId)
$_SERVER['HTTP_HOST']
Definition: raiseError.php:10
$param
Definition: xapitoken.php:29
const CLIENT_ID
Definition: constants.php:39
global $DIC
Definition: goto.php:24
CMI5preLaunch($token)
Prelaunch post cmi5LearnerPreference (agent profile) post LMS.LaunchData.
static guidv4($data=null)
static fillToken($usrId, $refId, $objId, $lrsTypeId=0)
$lang
Definition: xapiexit.php:8
static _isAnonymous($usr_id)
static checkResponse($response, &$body, $allowedStatus=[200, 204])
$ret
Definition: parser.php:6
static getIdent($userIdentMode, ilObjUser $user)
static buildQuery(array $params, $encoding=PHP_QUERY_RFC3986)
$response
static getWebspaceDir($mode="filesystem")
get webspace directory
static dateIntervalToISO860Duration(\DateInterval $d)