ILIAS  release_10 Revision v10.1-43-ga1241a92c2f
class.ilLTIConsumerResultService.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
25 
35 {
39  protected ?ilLTIConsumerResult $result = null;
40 
44  protected int $availability = 0;
45 
49  protected float $mastery_score = 1;
50 
54  protected array $fields = array();
55 
59  protected string $message_ref_id = '';
63  protected string $operation = '';
64 
65 
66  public function getMasteryScore(): float
67  {
68  return $this->mastery_score;
69  }
70 
71  public function setMasteryScore(float $mastery_score): void
72  {
73  $this->mastery_score = $mastery_score;
74  }
75 
76  public function getAvailability(): int
77  {
78  return $this->availability;
79  }
80 
81  public function setAvailability(int $availability): void
82  {
83  $this->availability = $availability;
84  }
85 
86  public function isAvailable(): bool
87  {
88  if ($this->availability == 0) {
89  return false;
90  }
91  return true;
92  }
93 
97  public function handleRequest(): void
98  {
99  try {
100 
101  global $DIC;
102  $logger = $DIC->logger()->root();
103  $logger->info('LTI Consumer Result Service: Incoming request');
104  // get the request as xml
105  $xml = simplexml_load_file('php://input');
106  $logger->info('LTI Consumer Result Service: xml loaded');
107  $this->message_ref_id = (string) $xml->imsx_POXHeader->imsx_POXRequestHeaderInfo->imsx_messageIdentifier;
108  $children = (array) $xml->imsx_POXBody->children();
109  $request = current($children);
110 
111  $ns = $xml->getNamespaces(true);
112  $body = $xml->children($ns[''])->imsx_POXBody;
113 
114  $logger->info('LTI Consumer Result Service: request loaded');
115  $this->operation = str_replace('Request', '', $request->getName());
116 
117  $request = $body->replaceResultRequest;
118  $token = ilCmiXapiAuthToken::getInstanceByToken((string) $request->resultRecord->sourcedGUID->sourcedId);
119  $logger->info("LTI Consumer Result Service: operation loaded ($this->operation), user " . $token->getUsrId() . " and objId " . $token->getObjId());
120 
121  $logger->info("LTI Consumer Result Service: token loaded");
122  $this->result = ilLTIConsumerResult::getByKeys($token->getObjId(), $token->getUsrId(), false);
123  if (empty($this->result)) {
124  $logger->error('LTI Consumer Result Service: Incoming request');
125  $this->respondUnauthorized("lti_consumer_results_id not found!");
126  return;
127  }
128 
129 
130  // check the object status
131  $this->readProperties($this->result->obj_id);
132 
133  if (!$this->isAvailable()) {
134  $this->respondUnsupported();
135  return;
136  }
137 
138  // Verify the signature
139  $this->readFields($this->result->obj_id);
140  $result = $this->checkSignature($this->fields['KEY'], $this->fields['SECRET']);
141  if ($result instanceof Exception) {
142  $logger->error('LTI Consumer Result Service: Incoming request');
143  $this->respondUnauthorized($result->getMessage());
144  return;
145  }
146 
147  $logger->info("LTI Consumer Result Service: Request signature verified, this->operation: $this->operation");
148 
149  // Dispatch the operation
150  switch ($this->operation) {
151  case 'readResult':
152  $this->readResult($request);
153  break;
154 
155  case 'replaceResult':
156  $this->replaceResult($request);
157  $this->updateLP();
158  break;
159 
160  case 'deleteResult':
161  $this->deleteResult($request);
162  $this->updateLP();
163  break;
164 
165  default:
166  $this->respondUnknown();
167  break;
168  }
169  } catch (Exception $exception) {
170  $this->respondBadRequest($exception->getMessage());
171  }
172  }
173 
177  protected function readResult(\SimpleXMLElement $request): void
178  {
179  $response = $this->loadResponse('readResult.xml');
180  $response = str_replace('{message_id}', md5((string) rand(0, 999_999_999)), $response);
181  $response = str_replace('{message_ref_id}', $this->message_ref_id, $response);
182  $response = str_replace('{operation}', $this->operation, $response);
183  $response = str_replace('{result}', (string) $this->result->result, $response);
184 
185  header('Content-type: application/xml');
186  echo $response;
187  }
188 
192  protected function replaceResult(\SimpleXMLElement $request): void
193  {
194  global $DIC;
195  $logger = $DIC->logger()->root();
196 
197  $result = (string) $request->resultRecord->result->resultScore->textString;
198  $logger->info('LTI Consumer Result Service: Replace result. Result: ' . $result);
199  if (!is_numeric($result)) {
200  $code = "failure";
201  $severity = "status";
202  $description = "The result is not a number.";
203  } elseif ($result > 1) {
204  $code = "failure";
205  $severity = "status";
206  $description = "The result is out of range from 0 to 1.";
207  } else {
208  $this->result->result = (float) $result;
209  $this->result->save();
210 
211  if ($result >= $this->getMasteryScore()) {
213  } else {
215  }
216  $lp_percentage = (int) round(100 * $result);
217 
218  // Mantis #37080
219  ilLPStatus::writeStatus($this->result->obj_id, $this->result->usr_id, $lp_status, $lp_percentage, true);
220 
221  $code = "success";
222  $severity = "status";
223  $description = sprintf("Score for %s is now %s", $this->result->id, $this->result->result);
224  }
225 
226  $response = $this->loadResponse('replaceResult.xml');
227  $response = str_replace('{message_id}', md5((string) rand(0, 999_999_999)), $response);
228  $response = str_replace('{message_ref_id}', $this->message_ref_id, $response);
229  $response = str_replace('{operation}', $this->operation, $response);
230  $response = str_replace('{code}', $code, $response);
231  $response = str_replace('{severity}', $severity, $response);
232  $response = str_replace('{description}', $description, $response);
233 
234  header('Content-type: application/xml');
235  echo $response;
236  }
237 
241  protected function deleteResult(\SimpleXMLElement $request): void
242  {
243  $this->result->result = null;
244  $this->result->save();
245 
247  $lp_percentage = 0;
248  ilLPStatus::writeStatus($this->result->obj_id, $this->result->usr_id, $lp_status, $lp_percentage, true);
249 
250  $code = "success";
251  $severity = "status";
252 
253  $response = $this->loadResponse('deleteResult.xml');
254  $response = str_replace('{message_id}', md5((string) rand(0, 999_999_999)), $response);
255  $response = str_replace('{message_ref_id}', $this->message_ref_id, $response);
256  $response = str_replace('{operation}', $this->operation, $response);
257  $response = str_replace('{code}', $code, $response);
258  $response = str_replace('{severity}', $severity, $response);
259 
260  header('Content-type: application/xml');
261  echo $response;
262  }
263 
264 
270  protected function loadResponse($a_name): string
271  {
272  return file_get_contents(__DIR__ . '/../responses/' . $a_name);
273  }
274 
275 
280  protected function respondUnsupported(): void
281  {
282  $response = $this->loadResponse('unsupported.xml');
283  $response = str_replace('{message_id}', md5((string) rand(0, 999_999_999)), $response);
284  $response = str_replace('{message_ref_id}', $this->message_ref_id, $response);
285  $response = str_replace('{operation}', $this->operation, $response);
286 
287  header('Content-type: application/xml');
288  echo $response;
289  }
290 
294  protected function respondUnknown(): void
295  {
296  $response = $this->loadResponse('unknown.xml');
297  $response = str_replace('{message_id}', md5((string) rand(0, 999_999_999)), $response);
298  $response = str_replace('{message_ref_id}', $this->message_ref_id, $response);
299  $response = str_replace('{operation}', $this->operation, $response);
300 
301  header('Content-type: application/xml');
302  echo $response;
303  }
304 
308  protected function respondBadRequest(?string $message = null): void
309  {
310  header('HTTP/1.1 400 Bad Request');
311  header('Content-type: text/plain');
312  if (isset($message)) {
313  echo $message;
314  } else {
315  echo 'This is not a well-formed LTI Basic Outcomes Service request.';
316  }
317  }
318 
323  protected function respondUnauthorized(?string $message = null): void
324  {
325  header('HTTP/1.1 401 Unauthorized');
326  header('Content-type: text/plain');
327  if (isset($message)) {
328  echo $message;
329  } else {
330  echo 'This request could not be authorized.';
331  }
332  }
333 
337  public function readProperties(int $a_obj_id): void
338  {
339  global $DIC;
340 
341  $query = "
342  SELECT lti_ext_provider.availability, lti_consumer_settings.mastery_score
343  FROM lti_ext_provider, lti_consumer_settings
344  WHERE lti_ext_provider.id = lti_consumer_settings.provider_id
345  AND lti_consumer_settings.obj_id = %s
346  ";
347 
348  $res = $DIC->database()->queryF($query, array('integer'), array($a_obj_id));
349 
350  if ($row = $DIC->database()->fetchAssoc($res)) {
351  //$this->properties = $row;
352  $this->setAvailability((int) $row['availability']);
353  $this->setMasteryScore((float) $row['mastery_score']);
354  }
355  }
356 
360  private function readFields(int $a_obj_id): void
361  {
362  global $DIC;
363 
364  $query = "
365  SELECT lti_ext_provider.provider_key, lti_ext_provider.provider_secret, lti_consumer_settings.launch_key, lti_consumer_settings.launch_secret
366  FROM lti_ext_provider, lti_consumer_settings
367  WHERE lti_ext_provider.id = lti_consumer_settings.provider_id
368  AND lti_consumer_settings.obj_id = %s
369  ";
370 
371  $res = $DIC->database()->queryF($query, array('integer'), array($a_obj_id));
372 
373  while ($row = $DIC->database()->fetchAssoc($res)) {
374  if (strlen($row["launch_key"]) > 0) {
375  $this->fields["KEY"] = $row["launch_key"];
376  } else {
377  $this->fields["KEY"] = $row["provider_key"];
378  }
379  if (strlen($row["launch_key"]) > 0) {
380  $this->fields["SECRET"] = $row["launch_secret"];
381  } else {
382  $this->fields["SECRET"] = $row["provider_secret"];
383  }
384  }
385  }
386 
391  private function checkSignature(string $a_key, string $a_secret): bool
392  {
393  $platform = new ilLTIPlatform();
394 
395  $platform->setKey($a_key);
396  $platform->setSecret($a_secret);
397 
398  $store = new OAuthDataStore($platform);
399 
400  $server = new OAuthServer($store);
401  $method = new OAuthSignatureMethod_HMAC_SHA1();
402  $server->add_signature_method($method);
403 
404  $request = OAuthRequest::from_request();
405 
406  try {
407  $server->verify_request($request);
408  } catch (Exception $e) {
409  return false;
410  }
411  return true;
412  }
413 
414  protected function updateLP(): void
415  {
416  if (!($this->result instanceof ilLTIConsumerResult)) {
417  return;
418  }
419 
420  ilLPStatusWrapper::_updateStatus($this->result->getObjId(), $this->result->getUsrId());
421  }
422 }
const LP_STATUS_COMPLETED_NUM
readFields(int $a_obj_id)
Read the LTI Consumer object fields.
respondUnknown()
Send a "unknown operation" response.
$res
Definition: ltiservices.php:69
respondBadRequest(?string $message=null)
Send a "bad request" response.
checkSignature(string $a_key, string $a_secret)
Check the reqest signature.
static getInstanceByToken(string $token)
deleteResult(\SimpleXMLElement $request)
Delete a stored result.
const LP_STATUS_IN_PROGRESS_NUM
$response
Definition: xapitoken.php:90
respondUnauthorized(?string $message=null)
Send an "unauthorized" response.
readProperties(int $a_obj_id)
Read the LTI Consumer object properties.
save()
Save a result object.
LTI provider for LTI launch.
$token
Definition: xapitoken.php:67
handleRequest()
Handle an incoming request from the LTI tool provider.
respondUnsupported()
Send a response that the operation is not supported This depends on the status of the object...
global $DIC
Definition: shib_login.php:25
static getByKeys(int $a_obj_id, int $a_usr_id, ?bool $a_create=false)
Get a result by object and user key.
loadResponse($a_name)
Load the XML template for the response.
static writeStatus(int $a_obj_id, int $a_user_id, int $a_status, int $a_percentage=0, bool $a_force_per=false, ?int &$a_old_status=self::LP_STATUS_NOT_ATTEMPTED_NUM)
Write status for user and object.
$server
Definition: shib_login.php:27
header()
expected output: > ILIAS shows the rendered Component.
Definition: header.php:13
replaceResult(\SimpleXMLElement $request)
Replace a stored result.
static _updateStatus(int $a_obj_id, int $a_usr_id, ?object $a_obj=null, bool $a_percentage=false, bool $a_force_raise=false)
readResult(\SimpleXMLElement $request)
Read a stored result.