ILIAS  trunk Revision v12.0_alpha-377-g3641b37b9db
class.ilSoapTestAdministration.php
Go to the documentation of this file.
1<?php
2
21
30{
32 public function __construct(bool $use_nusoap = true)
33 {
34 $this->questionrepository = TestDIC::dic()['general_question_properties_repository'];
35 parent::__construct($use_nusoap);
36 }
37 private function hasWritePermissionForTest(int $active_id): bool
38 {
39 global $DIC;
40
41 $ilDB = $DIC['ilDB'];
42 global $DIC;
43
44 $ilAccess = $DIC['ilAccess'];
45
46 $permission_ok = false;
47 $result = $ilDB->queryF(
48 "SELECT tst_tests.obj_fi FROM tst_active, tst_tests WHERE tst_active.active_id = %s AND tst_active.test_fi = tst_tests.test_id",
49 array('integer'),
50 array($active_id)
51 );
52 $row = $ilDB->fetchAssoc($result);
53 if ($row['obj_fi']) {
54 $obj_id = $row['obj_fi'];
55 foreach ($ref_ids = ilObject::_getAllReferences($obj_id) as $ref_id) {
56 if ($ilAccess->checkAccess("write", "", $ref_id)) {
57 $permission_ok = true;
58 break;
59 }
60 }
61 }
62 return $permission_ok;
63 }
64
65 public function isAllowedCall(string $sid, int $active_id, bool $saveaction = true): bool
66 {
67 global $DIC;
68
69 $ilDB = $DIC['ilDB'];
70 global $DIC;
71
72 $ilUser = $DIC['ilUser'];
73
74 if ($this->hasWritePermissionForTest($active_id)) {
75 return true;
76 }
77
78 if ($saveaction) {
79 $result = $ilDB->queryF(
80 "SELECT * FROM tst_times WHERE active_fi = %s ORDER BY started DESC",
81 array('integer'),
82 array($active_id)
83 );
84 if ($result->numRows()) {
85 $row = $ilDB->fetchAssoc($result);
86 if (preg_match("/(\\d{4})-(\\d{2})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2})/", $row["started"], $matches)) {
87 $time = mktime($matches[4], $matches[5], $matches[6], $matches[2], $matches[3], $matches[1]);
88 $now = time();
89 $diff = $now - $time;
90 $client = explode("::", $sid);
91 global $DIC;
92
93 $ilClientIniFile = $DIC['ilClientIniFile'];
94 $expires = $ilClientIniFile->readVariable('session', 'expire');
95 return $diff <= $expires;
96 }
97
98 return false;
99 }
100
101 return false;
102 }
103
104 $result = $ilDB->queryF(
105 "SELECT user_fi FROM tst_active WHERE active_id = %s",
106 array('integer'),
107 array($active_id)
108 );
109 $row = $ilDB->fetchAssoc($result);
110
111 return (int) $row['user_fi'] === $ilUser->getId();
112 }
113
117 public function saveQuestion(string $sid, int $active_id, int $question_id, int $pass, array $solution)
118 {
119 $this->initAuth($sid);
120 $this->initIlias();
121
122 if (!$this->checkSession($sid)) {
123 return $this->raiseError($this->getMessage(), $this->getMessageCode());
124 }
125 if (!$this->isAllowedCall($sid, $active_id)) {
126 return $this->raiseError("The required user information is only available for active users.", "");
127 }
128
129 global $DIC;
130
131 $ilDB = $DIC['ilDB'];
132 $ilUser = $DIC['ilUser'];
133
134 $processLockerFactory = new ilAssQuestionProcessLockerFactory(new ilSetting('assessment'), $ilDB, ilLoggerFactory::getLogger('tst'));
135 $processLockerFactory->setQuestionId($question_id);
136 $processLockerFactory->setUserId($ilUser->getId());
137 $processLocker = $processLockerFactory->getLocker();
138
139 $totalrows = 0;
140
141 $processLocker->executePersistWorkingStateLockOperation(function () use (
142 &$totalrows,
143 $processLocker,
144 $active_id,
145 $question_id,
146 $pass,
147 $solution
148 ) {
149 $processLocker->executeUserSolutionUpdateLockOperation(function () use (
150 &$totalrows,
151 $active_id,
152 $question_id,
153 $pass,
154 $solution
155 ) {
156 $ilDB = $GLOBALS['DIC']['ilDB'];
157 if (($active_id > 0) && ($question_id > 0) && ($pass > 0)) {
158 $affectedRows = $ilDB->manipulateF(
159 "DELETE FROM tst_solutions WHERE active_fi = %s AND question_fi = %s AND pass = %s",
160 array('integer', 'integer', 'integer'),
161 array($active_id, $question_id, $pass)
162 );
163 }
164 for ($i = 0, $iMax = count($solution); $i < $iMax; $i += 3) {
165 $next_id = $ilDB->nextId('tst_solutions');
166 $affectedRows = $ilDB->insert("tst_solutions", array(
167 "solution_id" => array("integer", $next_id),
168 "active_fi" => array("integer", $active_id),
169 "question_fi" => array("integer", $question_id),
170 "value1" => array("clob", $solution[$i]),
171 "value2" => array("clob", $solution[$i + 1]),
172 "points" => array("float", $solution[$i + 2]),
173 "pass" => array("integer", $pass),
174 "tstamp" => array("integer", time())
175 ));
176 $totalrows += $affectedRows;
177 }
178 });
179
180 if ($totalrows !== 0) {
181 $question = assQuestion::instantiateQuestion($question_id);
182 $question->setProcessLocker($processLocker);
183 $question->calculateResultsFromSolution($active_id, $pass);
184 }
185 });
186
187 if ($totalrows === 0) {
188 return $this->raiseError(
189 "Wrong solution data. ILIAS did not execute any database queries: Solution data: " . print_r(
190 $solution,
191 true
192 ),
193 'No result'
194 );
195 }
196 return true;
197 }
198
202 public function saveQuestionSolution(string $sid, int $active_id, int $question_id, int $pass, int $solution)
203 {
204 $this->initAuth($sid);
205 $this->initIlias();
206
207 if (!$this->checkSession($sid)) {
208 return $this->raiseError($this->getMessage(), $this->getMessageCode());
209 }
210 if (!$this->isAllowedCall($sid, $active_id)) {
211 return $this->raiseError("The required user information is only available for active users.", "");
212 }
213
214 $solutions = [];
215 if (preg_match("/<values>(.*?)<\/values>/is", $solution, $matches)) {
216 if (preg_match_all(
217 "/<value>(.*?)<\/value><value>(.*?)<\/value><points>(.*?)<\/points>/is",
218 $solution,
219 $matches,
220 PREG_SET_ORDER
221 )) {
222 foreach ($matches as $match) {
223 if (count($match) === 4) {
224 for ($i = 1, $iMax = count($match); $i < $iMax; $i++) {
225 $solutions[] = trim($match[$i]);
226 }
227 }
228 }
229 }
230 }
231
232 if (count($solutions) === 0) {
233 return $this->raiseError(
234 "Wrong solution data. ILIAS did not find one or more solution triplets: $solution",
235 ""
236 );
237 }
238
239 $ilDB = $GLOBALS['DIC']['ilDB'];
240 if (($active_id > 0) && ($question_id > 0) && ($pass > 0)) {
241 $affectedRows = $ilDB->manipulateF(
242 "DELETE FROM tst_solutions WHERE active_fi = %s AND question_fi = %s AND pass = %s",
243 array('integer', 'integer', 'integer'),
244 array($active_id, $question_id, $pass)
245 );
246 }
247 $totalrows = 0;
248 for ($i = 0, $iMax = count($solutions); $i < $iMax; $i += 3) {
249 $next_id = $ilDB->nextId('tst_solutions');
250 $affectedRows = $ilDB->insert("tst_solutions", array(
251 "solution_id" => array("integer", $next_id),
252 "active_fi" => array("integer", $active_id),
253 "question_fi" => array("integer", $question_id),
254 "value1" => array("clob", $solutions[$i]),
255 "value2" => array("clob", $solutions[$i + 1]),
256 "points" => array("float", $solutions[$i + 2]),
257 "pass" => array("integer", $pass),
258 "tstamp" => array("integer", time())
259 ));
260 $totalrows += $affectedRows;
261 }
262 if ($totalrows === 0) {
263 return $this->raiseError("Wrong solution data. ILIAS did not execute any database queries", '');
264 }
265
266 $question = assQuestion::instantiateQuestion($question_id);
267 $question->calculateResultsFromSolution($active_id, $pass);
268 return "TRUE";
269 }
270
274 public function getQuestionSolution(string $sid, int $active_id, int $question_id, int $pass)
275 {
276 $this->initAuth($sid);
277 $this->initIlias();
278
279 if (!$this->checkSession($sid)) {
280 return $this->raiseError($this->getMessage(), $this->getMessageCode());
281 }
282 if (!$this->isAllowedCall($sid, $active_id, false)) {
283 return $this->raiseError("The required user information is only available for active users.", "");
284 }
285 $solution = array();
286
287 global $DIC;
288
289 $ilDB = $DIC['ilDB'];
290
291 $test_obj_id = ilObjTest::_lookupTestObjIdForQuestionId($question_id);
292 $test_obj = new ilObjTest($test_obj_id, false);
293 $use_previous_answers = $test_obj->getUsePreviousAnswers();
294
295 $lastpass = 0;
296 if ($use_previous_answers) {
297 $result = $ilDB->queryF(
298 "SELECT MAX(pass) maxpass FROM tst_test_result WHERE active_fi = %s AND question_fi = %s",
299 array('integer', 'integer'),
300 array($active_id, $question_id)
301 );
302 if ($result->numRows() === 1) {
303 $row = $ilDB->fetchAssoc($result);
304 $lastpass = (int) $row["maxpass"];
305 }
306 } else {
307 $lastpass = $pass;
308 }
309
310 if (($active_id > 0) && ($question_id > 0) && ($lastpass > 0)) {
311 $result = $ilDB->queryF(
312 "SELECT * FROM tst_solutions WHERE active_fi = %s AND question_fi = %s AND pass = %s",
313 array('integer', 'integer', 'integer'),
314 array($active_id, $question_id, $lastpass)
315 );
316 if ($result->numRows()) {
317 while ($row = $ilDB->fetchAssoc($result)) {
318 $solution[] = $row["value1"];
319 $solution[] = $row["value2"];
320 $solution[] = $row["points"];
321 }
322 }
323 }
324 return $solution;
325 }
326
330 public function getTestUserData(string $sid, int $active_id)
331 {
332 $this->initAuth($sid);
333 $this->initIlias();
334
335 if (!$this->checkSession($sid)) {
336 return $this->raiseError($this->getMessage(), $this->getMessageCode());
337 }
338 if (!$this->isAllowedCall($sid, $active_id, false)) {
339 return $this->raiseError("The required user information is only available for active users.", "");
340 }
341
342 global $DIC;
343
344 $lng = $DIC['lng'];
345 $ilDB = $DIC['ilDB'];
346
347 $result = $ilDB->queryF(
348 "SELECT user_fi, test_fi FROM tst_active WHERE active_id = %s",
349 array('integer'),
350 array($active_id)
351 );
352 $row = $ilDB->fetchAssoc($result);
353 $user_id = $row["user_fi"];
354 $test_id = $row["test_fi"];
355
356 $test_obj_id = ilObjTest::_getObjectIDFromTestID($test_id);
357 $test_obj = new ilObjTest($test_obj_id, false);
358 $anonymity = $test_obj->getAnonymity();
359
360 $result = $ilDB->queryF(
361 "SELECT firstname, lastname, title, login FROM usr_data WHERE usr_id = %s",
362 array('integer'),
363 array($user_id)
364 );
365
366 $userdata = array();
367 if ($result->numRows() === 0) {
368 $userdata["fullname"] = $lng->txt("deleted_user");
369 $userdata["title"] = "";
370 $userdata["firstname"] = "";
371 $userdata["lastname"] = $lng->txt("anonymous");
372 $userdata["login"] = "";
373 } else {
374 $data = $ilDB->fetchAssoc($result);
375 if ((int) $user_id === ANONYMOUS_USER_ID || $anonymity) {
376 $userdata["fullname"] = $lng->txt("anonymous");
377 $userdata["title"] = "";
378 $userdata["firstname"] = "";
379 $userdata["lastname"] = $lng->txt("anonymous");
380 $userdata["login"] = "";
381 } else {
382 $userdata["fullname"] = trim($data["title"] . " " . $data["firstname"] . " " . $data["lastname"]);
383 $userdata["title"] = $data["title"];
384 $userdata["firstname"] = $data["firstname"];
385 $userdata["lastname"] = $data["lastname"];
386 $userdata["login"] = $data["login"];
387 }
388 }
389 return array_values($userdata);
390 }
391
395 public function getPositionOfQuestion(string $sid, int $active_id, int $question_id, int $pass)
396 {
397 $this->initAuth($sid);
398 $this->initIlias();
399
400 if (!$this->checkSession($sid)) {
401 return $this->raiseError($this->getMessage(), $this->getMessageCode());
402 }
403 if (!$this->isAllowedCall($sid, $active_id, false)) {
404 return $this->raiseError("The required user information is only available for active users.", "");
405 }
406
407 global $DIC;
408 $ilDB = $DIC['ilDB'];
409
410 $sequence = new ilTestSequence($ilDB, $active_id, $pass, $this->questionrepository);
411 return $sequence->getSequenceForQuestion($question_id);
412 }
413
417 public function getPreviousReachedPoints(string $sid, int $active_id, int $question_id, int $pass)
418 {
419 $this->initAuth($sid);
420 $this->initIlias();
421
422 if (!$this->checkSession($sid)) {
423 return $this->raiseError($this->getMessage(), $this->getMessageCode());
424 }
425 if (!$this->isAllowedCall($sid, $active_id, false)) {
426 return $this->raiseError("The required user information is only available for active users.", "");
427 }
428
429 global $DIC;
430
431 $lng = $DIC['lng'];
432 $ilDB = $DIC['ilDB'];
433
434 $sequence = new ilTestSequence($ilDB, $active_id, $pass, $this->questionrepository);
435 $result = $ilDB->queryF(
436 "SELECT question_fi, points FROM tst_test_result WHERE active_fi = %s AND pass = %s",
437 array('integer', 'integer'),
438 array($active_id, $pass)
439 );
440 $reachedpoints = array();
441 while ($row = $ilDB->fetchAssoc($result)) {
442 $reachedpoints[$row["question_fi"]] = $row["points"];
443 }
444 $atposition = false;
445 $pointsforposition = array();
446 foreach ($sequence->getUserSequence() as $seq) {
447 if (!$atposition) {
448 $qid = $sequence->getQuestionForSequence($seq);
449 if ($qid == $question_id) {
450 $atposition = true;
451 } else {
452 $pointsforposition[] = $reachedpoints[$qid];
453 }
454 }
455 }
456 return $pointsforposition;
457 }
458
462 public function getNrOfQuestionsInPass(string $sid, int $active_id, int $pass)
463 {
464 $this->initAuth($sid);
465 $this->initIlias();
466
467 if (!$this->checkSession($sid)) {
468 return $this->raiseError($this->getMessage(), $this->getMessageCode());
469 }
470 if (!$this->isAllowedCall($sid, $active_id, false)) {
471 return $this->raiseError("The required user information is only available for active users.", "");
472 }
473
474 global $DIC;
475
476 $lng = $DIC['lng'];
477 $ilDB = $DIC['ilDB'];
478
479 $sequence = new ilTestSequence($ilDB, $active_id, $pass, $this->questionrepository);
480 return $sequence->getUserQuestionCount();
481 }
482
486 public function removeTestResults(string $sid, int $test_ref_id, array $a_user_ids)
487 {
488 $this->initAuth($sid);
489 $this->initIlias();
490
491 if (!$this->checkSession($sid)) {
492 return $this->raiseError($this->getMessage(), $this->getMessageCode());
493 }
494 if (!($test_ref_id > 0)) {
495 return $this->raiseError(
496 'No test id given. Aborting!',
497 'Client'
498 );
499 }
500 global $DIC;
501
502 $rbacsystem = $DIC['rbacsystem'];
503 $tree = $DIC['tree'];
504 $ilLog = $DIC['ilLog'];
505
506 if (!$this->checkManageParticipantsAccess($test_ref_id)) {
507 return $this->raiseError('no permission. Aborting!', 'Client');
508 }
509
510 if (ilObject::_isInTrash($test_ref_id)) {
511 return $this->raiseError(
512 'Test is trashed. Aborting!',
513 'Client'
514 );
515 }
516
517 if (!$tst = ilObjectFactory::getInstanceByRefId($test_ref_id, false)) {
518 return $this->raiseError('No test found for id: ' . $test_ref_id, 'Client');
519 }
520 if ($tst->getType() !== 'tst') {
521 return $this->raiseError(
522 'Object with ref_id ' . $test_ref_id . ' is not of type test. Aborting',
523 'Client'
524 );
525 }
526
527 // Dirty hack
528 if (isset($a_user_ids['item'])) {
529 $a_user_ids = $a_user_ids['item'];
530 }
531
532 $part = new ilTestParticipantData($GLOBALS['DIC']['ilDB'], $GLOBALS['DIC']['lng']);
533 $part->setParticipantAccessFilter(
534 ilTestParticipantAccessFilter::getManageParticipantsUserFilter($test_ref_id)
535 );
536 $part->setUserIdsFilter((array) $a_user_ids);
537 $part->load($tst->getTestId());
538 $tst->removeTestResults($part);
539
540 return true;
541 }
542
546 public function getTestResults(string $sid, int $test_ref_id, bool $sum_only)
547 {
548 $this->initAuth($sid);
549 $this->initIlias();
550
551 if (!$this->checkSession($sid)) {
552 return $this->raiseError($this->getMessage(), $this->getMessageCode());
553 }
554 if (!($test_ref_id > 0)) {
555 return $this->raiseError(
556 'No test id given. Aborting!',
557 'Client'
558 );
559 }
560 global $DIC;
561
562 $rbacsystem = $DIC['rbacsystem'];
563 $tree = $DIC['tree'];
564 $ilLog = $DIC['ilLog'];
565
566 if (ilObject::_isInTrash($test_ref_id)) {
567 return $this->raiseError(
568 'Test is trashed. Aborting!',
569 'Client'
570 );
571 }
572
573 if (!$obj_id = ilObject::_lookupObjectId($test_ref_id)) {
574 return $this->raiseError(
575 'No test found for id: ' . $test_ref_id,
576 'Client'
577 );
578 }
579
580 $permission_ok = false;
581 foreach ($ref_ids = ilObject::_getAllReferences($obj_id) as $ref_id) {
582 if ($rbacsystem->checkAccess('write', $ref_id)) {
583 $permission_ok = true;
584 break;
585 }
586 }
587 if (!$permission_ok && $this->checkParticipantsResultsAccess($test_ref_id)) {
588 $permission_ok = $this->checkParticipantsResultsAccess($test_ref_id);
589 }
590
591 if (!$permission_ok) {
592 return $this->raiseError(
593 'No permission to edit the object with id: ' . $test_ref_id,
594 'Server'
595 );
596 }
597
598 $xmlResultSet = new ilXMLResultSet();
599 $xmlResultSet->addColumn("user_id");
600 $xmlResultSet->addColumn("login");
601 $xmlResultSet->addColumn("firstname");
602 $xmlResultSet->addColumn("lastname");
603 $xmlResultSet->addColumn("matriculation");
604
605 $test_obj = new ilObjTest($obj_id, false);
606 $participants = $test_obj->getTestParticipants();
607
608 $accessFilter = ilTestParticipantAccessFilter::getAccessResultsUserFilter($test_ref_id);
609 $participantList = new ilTestParticipantList($test_obj);
610 $participantList->initializeFromDbRows($participants);
611 $participantList = $participantList->getAccessFilteredList($accessFilter);
612 $participantList = $participantList->getScoredParticipantList();
613 foreach ($participants as $activeId => $part) {
614 if ($participantList->isActiveIdInList($activeId)) {
615 $participants[$activeId]['passed'] = $participantList->getParticipantByActiveId($activeId)->getScoring()->isPassed();
616 continue;
617 }
618
619 unset($participants[$activeId]);
620 }
621
622 if ($sum_only) {
623 $data = $test_obj->getAllTestResults($participants);
624
625 $xmlResultSet->addColumn("maximum_points");
626 $xmlResultSet->addColumn("received_points");
627 $xmlResultSet->addColumn("passed");
628 // skip titles
629 $titles = array_shift($data);
630 foreach ($data as $row) {
631 $xmlRow = new ilXMLResultSetRow();
632 $xmlRow->setValue(0, $row["user_id"]);
633 $xmlRow->setValue(1, $row["login"]);
634 $xmlRow->setValue(2, $row["firstname"]);
635 $xmlRow->setValue(3, $row["lastname"]);
636 $xmlRow->setValue(4, $row["matriculation"]);
637 $xmlRow->setValue(5, $row["max_points"]);
638 $xmlRow->setValue(6, $row["reached_points"]);
639 $xmlRow->setValue(7, $row["passed"]);
640 $xmlResultSet->addRow($xmlRow);
641 }
642 } else {
643 $data = $test_obj->getDetailedTestResults($participants);
644
645 $xmlResultSet->addColumn("question_id");
646 $xmlResultSet->addColumn("question_title");
647 $xmlResultSet->addColumn("maximum_points");
648 $xmlResultSet->addColumn("received_points");
649 $xmlResultSet->addColumn("passed");
650 foreach ($data as $row) {
651 $xmlRow = new ilXMLResultSetRow();
652 $xmlRow->setValue(0, $row["user_id"]);
653 $xmlRow->setValue(1, $row["login"]);
654 $xmlRow->setValue(2, $row["firstname"]);
655 $xmlRow->setValue(3, $row["lastname"]);
656 $xmlRow->setValue(4, $row["matriculation"]);
657 $xmlRow->setValue(5, $row["question_id"]);
658 $xmlRow->setValue(6, $row["question_title"]);
659 $xmlRow->setValue(7, $row["max_points"]);
660 $xmlRow->setValue(8, $row["reached_points"]);
661 $xmlRow->setValue(9, $row["passed"]);
662 $xmlResultSet->addRow($xmlRow);
663 }
664 }
665
666 $xmlWriter = new ilXMLResultSetWriter($xmlResultSet);
667 $xmlWriter->start();
668 return $xmlWriter->getXML();
669 }
670
671 protected function checkManageParticipantsAccess(int $refId): bool
672 {
673 return $this->getTestAccess($refId)->checkManageParticipantsAccess();
674 }
675
676 protected function checkParticipantsResultsAccess(int $refId): bool
677 {
678 return $this->getTestAccess($refId)->checkParticipantsResultsAccess();
679 }
680
681 protected function getTestAccess(int $refId): ilTestAccess
682 {
683
685 return new ilTestAccess($refId, $testId);
686 }
687}
static instantiateQuestion(int $question_id)
static getLogger(string $a_component_id)
Get component logger.
static _getTestIDFromObjectID(int $object_id)
Returns the ILIAS test id for a given object id.
static _getObjectIDFromTestID($test_id)
Returns the ILIAS test object id for a given test id.
static _lookupTestObjIdForQuestionId(int $q_id)
Get test Object ID for question ID.
static getInstanceByRefId(int $ref_id, bool $stop_on_error=true)
get an instance of an Ilias object by reference id
static _lookupObjectId(int $ref_id)
static _getAllReferences(int $id)
get all reference ids for object ID
static _isInTrash(int $ref_id)
ILIAS Setting Class.
raiseError(string $a_message, $a_code)
saveQuestionSolution(string $sid, int $active_id, int $question_id, int $pass, int $solution)
getPreviousReachedPoints(string $sid, int $active_id, int $question_id, int $pass)
isAllowedCall(string $sid, int $active_id, bool $saveaction=true)
saveQuestion(string $sid, int $active_id, int $question_id, int $pass, array $solution)
getQuestionSolution(string $sid, int $active_id, int $question_id, int $pass)
getPositionOfQuestion(string $sid, int $active_id, int $question_id, int $pass)
removeTestResults(string $sid, int $test_ref_id, array $a_user_ids)
getTestResults(string $sid, int $test_ref_id, bool $sum_only)
GeneralQuestionPropertiesRepository $questionrepository
getTestUserData(string $sid, int $active_id)
getNrOfQuestionsInPass(string $sid, int $active_id, int $pass)
Test sequence handler.
Row Class for XMLResultSet.
XML Writer for XMLResultSet.
const ANONYMOUS_USER_ID
Definition: constants.php:27
$client
$ref_id
Definition: ltiauth.php:66
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
global $lng
Definition: privfeed.php:31
global $DIC
Definition: shib_login.php:26
$GLOBALS["DIC"]
Definition: wac.php:54
$refId
Definition: xapitoken.php:56