ILIAS  release_9 Revision v9.13-25-g2c18ec4c24f
class.ilLTIConsumerResultService.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 /******************************************************************************
5  *
6  * This file is part of ILIAS, a powerful learning management system.
7  *
8  * ILIAS is licensed with the GPL-3.0, you should have received a copy
9  * of said license along with the source code.
10  *
11  * If this is not the case or you just want to try ILIAS, you'll find
12  * us at:
13  * https://www.ilias.de
14  * https://github.com/ILIAS-eLearning
15  *
16  *****************************************************************************/
26 {
30  protected ?ilLTIConsumerResult $result = null;
31 
35  protected int $availability = 0;
36 
40  protected float $mastery_score = 1;
41 
45  protected array $fields = array();
46 
50  protected string $message_ref_id = '';
54  protected string $operation = '';
55 
56 
57  public function getMasteryScore(): float
58  {
59  return $this->mastery_score;
60  }
61 
62  public function setMasteryScore(float $mastery_score): void
63  {
64  $this->mastery_score = $mastery_score;
65  }
66 
67  public function getAvailability(): int
68  {
69  return $this->availability;
70  }
71 
72  public function setAvailability(int $availability): void
73  {
74  $this->availability = $availability;
75  }
76 
77  public function isAvailable(): bool
78  {
79  if ($this->availability == 0) {
80  return false;
81  }
82  return true;
83  }
84 
88  public function handleRequest(): void
89  {
90  try {
91  // get the request as xml
92  $xml = simplexml_load_file('php://input');
93  $this->message_ref_id = (string) $xml->imsx_POXHeader->imsx_POXRequestHeaderInfo->imsx_messageIdentifier;
94  // $request = current($xml->imsx_POXBody->children());
95  $request = $xml->imsx_POXBody->children()[0];
96  $this->operation = str_replace('Request', '', $request->getName());
97 
98  $token = ilCmiXapiAuthToken::getInstanceByToken((string) $request->resultRecord->sourcedGUID->sourcedId);
99 
100  $this->result = ilLTIConsumerResult::getByKeys($token->getObjId(), $token->getUsrId(), false);
101  if (empty($this->result)) {
102  $this->respondUnauthorized("lti_consumer_results_id not found!");
103  return;
104  }
105 
106 
107  // check the object status
108  $this->readProperties($this->result->obj_id);
109 
110  if (!$this->isAvailable()) {
111  $this->respondUnsupported();
112  return;
113  }
114 
115  // Verify the signature
116  $this->readFields($this->result->obj_id);
117  $result = $this->checkSignature($this->fields['KEY'], $this->fields['SECRET']);
118  if ($result instanceof Exception) {
119  $this->respondUnauthorized($result->getMessage());
120  return;
121  }
122 
123  // Dispatch the operation
124  switch ($this->operation) {
125  case 'readResult':
126  $this->readResult($request);
127  break;
128 
129  case 'replaceResult':
130  $this->replaceResult($request);
131  $this->updateLP();
132  break;
133 
134  case 'deleteResult':
135  $this->deleteResult($request);
136  $this->updateLP();
137  break;
138 
139  default:
140  $this->respondUnknown();
141  break;
142  }
143  } catch (Exception $exception) {
144  $this->respondBadRequest($exception->getMessage());
145  }
146  }
147 
151  protected function readResult(\SimpleXMLElement $request): void
152  {
153  $response = $this->loadResponse('readResult.xml');
154  $response = str_replace('{message_id}', md5((string) rand(0, 999_999_999)), $response);
155  $response = str_replace('{message_ref_id}', $this->message_ref_id, $response);
156  $response = str_replace('{operation}', $this->operation, $response);
157  $response = str_replace('{result}', (string) $this->result->result, $response);
158 
159  header('Content-type: application/xml');
160  echo $response;
161  }
162 
166  protected function replaceResult(\SimpleXMLElement $request): void
167  {
168  $result = (string) $request->resultRecord->result->resultScore->textString;
169  if (!is_numeric($result)) {
170  $code = "failure";
171  $severity = "status";
172  $description = "The result is not a number.";
173  } elseif ($result < 0 or $result > 1) {
174  $code = "failure";
175  $severity = "status";
176  $description = "The result is out of range from 0 to 1.";
177  } else {
178  $this->result->result = (float) $result;
179  $this->result->save();
180 
181  if ($result >= $this->getMasteryScore()) {
183  } else {
185  }
186  $lp_percentage = (int) round(100 * $result);
187 
188  // Mantis #37080
189  ilLPStatus::writeStatus($this->result->obj_id, $this->result->usr_id, $lp_status, $lp_percentage, true);
190 
191  $code = "success";
192  $severity = "status";
193  $description = sprintf("Score for %s is now %s", $this->result->id, $this->result->result);
194  }
195 
196  $response = $this->loadResponse('replaceResult.xml');
197  $response = str_replace('{message_id}', md5((string) rand(0, 999_999_999)), $response);
198  $response = str_replace('{message_ref_id}', $this->message_ref_id, $response);
199  $response = str_replace('{operation}', $this->operation, $response);
200  $response = str_replace('{code}', $code, $response);
201  $response = str_replace('{severity}', $severity, $response);
202  $response = str_replace('{description}', $description, $response);
203 
204  header('Content-type: application/xml');
205  echo $response;
206  }
207 
211  protected function deleteResult(\SimpleXMLElement $request): void
212  {
213  $this->result->result = null;
214  $this->result->save();
215 
217  $lp_percentage = 0;
218  ilLPStatus::writeStatus($this->result->obj_id, $this->result->usr_id, $lp_status, $lp_percentage, true);
219 
220  $code = "success";
221  $severity = "status";
222 
223  $response = $this->loadResponse('deleteResult.xml');
224  $response = str_replace('{message_id}', md5((string) rand(0, 999_999_999)), $response);
225  $response = str_replace('{message_ref_id}', $this->message_ref_id, $response);
226  $response = str_replace('{operation}', $this->operation, $response);
227  $response = str_replace('{code}', $code, $response);
228  $response = str_replace('{severity}', $severity, $response);
229 
230  header('Content-type: application/xml');
231  echo $response;
232  }
233 
234 
240  protected function loadResponse($a_name): string
241  {
242  return file_get_contents('./Modules/LTIConsumer/responses/' . $a_name);
243  }
244 
245 
250  protected function respondUnsupported(): void
251  {
252  $response = $this->loadResponse('unsupported.xml');
253  $response = str_replace('{message_id}', md5((string) rand(0, 999_999_999)), $response);
254  $response = str_replace('{message_ref_id}', $this->message_ref_id, $response);
255  $response = str_replace('{operation}', $this->operation, $response);
256 
257  header('Content-type: application/xml');
258  echo $response;
259  }
260 
264  protected function respondUnknown(): void
265  {
266  $response = $this->loadResponse('unknown.xml');
267  $response = str_replace('{message_id}', md5((string) rand(0, 999_999_999)), $response);
268  $response = str_replace('{message_ref_id}', $this->message_ref_id, $response);
269  $response = str_replace('{operation}', $this->operation, $response);
270 
271  header('Content-type: application/xml');
272  echo $response;
273  }
274 
278  protected function respondBadRequest(?string $message = null): void
279  {
280  header('HTTP/1.1 400 Bad Request');
281  header('Content-type: text/plain');
282  if (isset($message)) {
283  echo $message;
284  } else {
285  echo 'This is not a well-formed LTI Basic Outcomes Service request.';
286  }
287  }
288 
293  protected function respondUnauthorized(?string $message = null): void
294  {
295  header('HTTP/1.1 401 Unauthorized');
296  header('Content-type: text/plain');
297  if (isset($message)) {
298  echo $message;
299  } else {
300  echo 'This request could not be authorized.';
301  }
302  }
303 
307  public function readProperties(int $a_obj_id): void
308  {
309  global $DIC;
310 
311  $query = "
312  SELECT lti_ext_provider.availability, lti_consumer_settings.mastery_score
313  FROM lti_ext_provider, lti_consumer_settings
314  WHERE lti_ext_provider.id = lti_consumer_settings.provider_id
315  AND lti_consumer_settings.obj_id = %s
316  ";
317 
318  $res = $DIC->database()->queryF($query, array('integer'), array($a_obj_id));
319 
320  if ($row = $DIC->database()->fetchAssoc($res)) {
321  //$this->properties = $row;
322  $this->setAvailability((int) $row['availability']);
323  $this->setMasteryScore((float) $row['mastery_score']);
324  }
325  }
326 
330  private function readFields(int $a_obj_id): void
331  {
332  global $DIC;
333 
334  $query = "
335  SELECT lti_ext_provider.provider_key, lti_ext_provider.provider_secret, lti_consumer_settings.launch_key, lti_consumer_settings.launch_secret
336  FROM lti_ext_provider, lti_consumer_settings
337  WHERE lti_ext_provider.id = lti_consumer_settings.provider_id
338  AND lti_consumer_settings.obj_id = %s
339  ";
340 
341  $res = $DIC->database()->queryF($query, array('integer'), array($a_obj_id));
342 
343  while ($row = $DIC->database()->fetchAssoc($res)) {
344  if (strlen($row["launch_key"]) > 0) {
345  $this->fields["KEY"] = $row["launch_key"];
346  } else {
347  $this->fields["KEY"] = $row["provider_key"];
348  }
349  if (strlen($row["launch_key"]) > 0) {
350  $this->fields["SECRET"] = $row["launch_secret"];
351  } else {
352  $this->fields["SECRET"] = $row["provider_secret"];
353  }
354  }
355  }
356 
361  private function checkSignature(string $a_key, string $a_secret)
362  {
363  $store = new TrivialOAuthDataStore();
364  $store->add_consumer($a_key, $a_secret);
365 
366  $server = new \ILIAS\LTIOAuth\OAuthServer($store);
367  $method = new \ILIAS\LTIOAuth\OAuthSignatureMethod_HMAC_SHA1();
368  $server->add_signature_method($method);
369 
370  $request = \ILIAS\LTIOAuth\OAuthRequest::from_request();
371  try {
372  $server->verify_request($request);
373  } catch (Exception $e) {
374  return $e;
375  }
376  return true;
377  }
378 
379  protected function updateLP(): void
380  {
381  if (!($this->result instanceof ilLTIConsumerResult)) {
382  return;
383  }
384 
385  ilLPStatusWrapper::_updateStatus($this->result->getObjId(), $this->result->getUsrId());
386  }
387 }
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:93
respondUnauthorized(?string $message=null)
Send an "unauthorized" response.
readProperties(int $a_obj_id)
Read the LTI Consumer object properties.
save()
Save a result object.
global $DIC
Definition: feed.php:28
if(!file_exists(getcwd() . '/ilias.ini.php'))
Definition: confirmReg.php:21
$token
Definition: xapitoken.php:70
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...
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.
A Trivial memory-based store - no support for tokens.
$server
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.
$message
Definition: xapiexit.php:32
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.