lp_ajax_save_item.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. <?php
  2. /* For licensing terms, see /license.txt */
  3. use ChamiloSession as Session;
  4. /**
  5. * This script contains the server part of the AJAX interaction process.
  6. * The client part is located * in lp_api.php or other api's.
  7. *
  8. * @package chamilo.learnpath
  9. *
  10. * @author Yannick Warnier <ywarnier@beeznest.org>
  11. */
  12. // Flag to allow for anonymous user - needs to be set before global.inc.php.
  13. $use_anonymous = true;
  14. require_once __DIR__.'/../inc/global.inc.php';
  15. api_protect_course_script();
  16. /**
  17. * Writes an item's new values into the database and returns the operation result.
  18. *
  19. * @param int $lp_id Learnpath ID
  20. * @param int $user_id User ID
  21. * @param int $view_id View ID
  22. * @param int $item_id Item ID
  23. * @param float $score Current score
  24. * @param float $max Maximum score
  25. * @param float $min Minimum score
  26. * @param string $status Lesson status
  27. * @param int $time Session time
  28. * @param string $suspend Suspend data
  29. * @param string $location Lesson location
  30. * @param array $interactions Interactions array
  31. * @param string $core_exit Core exit SCORM string
  32. * @param int $sessionId Session ID
  33. * @param int $courseId Course ID
  34. * @param int $lmsFinish Whether the call was issued from SCORM's LMSFinish()
  35. * @param int $userNavigatesAway Whether the user is moving to another item
  36. * @param int $statusSignalReceived Whether the SCO called SetValue(lesson_status)
  37. *
  38. * @return bool|string|null The resulting JS string
  39. */
  40. function save_item(
  41. $lp_id,
  42. $user_id,
  43. $view_id,
  44. $item_id,
  45. $score = -1.0,
  46. $max = -1.0,
  47. $min = -1.0,
  48. $status = '',
  49. $time = 0,
  50. $suspend = '',
  51. $location = '',
  52. $interactions = [],
  53. $core_exit = 'none',
  54. $sessionId = null,
  55. $courseId = null,
  56. $lmsFinish = 0,
  57. $userNavigatesAway = 0,
  58. $statusSignalReceived = 0
  59. ) {
  60. $debug = 0;
  61. $return = null;
  62. if ($debug > 0) {
  63. error_log('--------------------------------------');
  64. error_log('lp_ajax_save_item.php : save_item() params: ');
  65. error_log("item_id: $item_id");
  66. error_log("lp_id: $lp_id - user_id: - $user_id - view_id: $view_id - item_id: $item_id");
  67. error_log("score: $score - max:$max - min: $min - status:$status");
  68. error_log("time:$time - suspend: $suspend - location: $location - core_exit: $core_exit");
  69. error_log("finish: $lmsFinish - navigatesAway: $userNavigatesAway");
  70. }
  71. $courseCode = api_get_course_id();
  72. if (!empty($courseId)) {
  73. $courseInfo = api_get_course_info_by_id($courseId);
  74. if ($courseInfo) {
  75. $courseCode = $courseInfo['code'];
  76. }
  77. }
  78. $myLP = learnpath::getLpFromSession($courseCode, $lp_id, $user_id);
  79. if (!is_a($myLP, 'learnpath')) {
  80. if ($debug) {
  81. error_log('mylp variable is not an learnpath object');
  82. }
  83. return null;
  84. }
  85. $prerequisitesCheck = $myLP->prerequisites_match($item_id);
  86. /** @var learnpathItem $myLPI */
  87. $myLPI = $myLP->items[$item_id];
  88. if (empty($myLPI)) {
  89. if ($debug > 0) {
  90. error_log("item #$item_id not found in the items array: ".print_r($myLP->items, 1));
  91. }
  92. return false;
  93. }
  94. // This functions sets the $this->db_item_view_id variable needed in get_status() see BT#5069
  95. $myLPI->set_lp_view($view_id);
  96. // Launch the prerequisites check and set error if needed
  97. if ($prerequisitesCheck !== true) {
  98. // If prerequisites were not matched, don't update any item info
  99. if ($debug) {
  100. error_log("prereq_check: ".intval($prerequisitesCheck));
  101. }
  102. return $return;
  103. } else {
  104. if ($debug > 1) {
  105. error_log('Prerequisites are OK');
  106. }
  107. $logInfo = [
  108. 'tool' => TOOL_LEARNPATH,
  109. 'tool_id' => $lp_id,
  110. 'tool_id_detail' => $item_id,
  111. 'action' => 'view',
  112. 'action_details' => $myLP->getCurrentAttempt(),
  113. ];
  114. Event::registerLog($logInfo);
  115. if (isset($max) && $max != -1) {
  116. $myLPI->max_score = $max;
  117. $myLPI->set_max_score($max);
  118. if ($debug > 1) {
  119. error_log("Setting max_score: $max");
  120. }
  121. }
  122. if (isset($min) && $min != -1 && $min != 'undefined') {
  123. $myLPI->min_score = $min;
  124. if ($debug > 1) {
  125. error_log("Setting min_score: $min");
  126. }
  127. }
  128. // set_score function used to save the status, but this is not the case anymore
  129. if (isset($score) && $score != -1) {
  130. if ($debug > 1) {
  131. error_log('Calling set_score('.$score.')');
  132. error_log('set_score changes the status to failed/passed if mastery score is provided');
  133. }
  134. $myLPI->set_score($score);
  135. if ($debug > 1) {
  136. error_log('Done calling set_score '.$myLPI->get_score());
  137. }
  138. } else {
  139. if ($debug > 1) {
  140. error_log('Score not updated');
  141. }
  142. }
  143. $statusIsSet = false;
  144. // Default behaviour.
  145. if (isset($status) && $status != '' && $status != 'undefined') {
  146. if ($debug > 1) {
  147. error_log('Calling set_status('.$status.')');
  148. }
  149. $myLPI->set_status($status);
  150. $statusIsSet = true;
  151. if ($debug > 1) {
  152. error_log('Done calling set_status: checking from memory: '.$myLPI->get_status(false));
  153. }
  154. } else {
  155. if ($debug > 1) {
  156. error_log('Status not updated');
  157. }
  158. }
  159. $my_type = $myLPI->get_type();
  160. // Set status to completed for hotpotatoes if score > 80%.
  161. if ($my_type == 'hotpotatoes') {
  162. if ((empty($status) || $status == 'undefined' || $status == 'not attempted') && $max > 0) {
  163. if (($score / $max) > 0.8) {
  164. $myStatus = 'completed';
  165. if ($debug > 1) {
  166. error_log('Calling set_status('.$myStatus.') for hotpotatoes');
  167. }
  168. $myLPI->set_status($myStatus);
  169. $statusIsSet = true;
  170. if ($debug > 1) {
  171. error_log('Done calling set_status for hotpotatoes - now '.$myLPI->get_status(false));
  172. }
  173. }
  174. } elseif ($status == 'completed' && $max > 0 && ($score / $max) < 0.8) {
  175. $myStatus = 'failed';
  176. if ($debug > 1) {
  177. error_log('Calling set_status('.$myStatus.') for hotpotatoes');
  178. }
  179. $myLPI->set_status($myStatus);
  180. $statusIsSet = true;
  181. if ($debug > 1) {
  182. error_log('Done calling set_status for hotpotatoes - now '.$myLPI->get_status(false));
  183. }
  184. }
  185. } elseif ($my_type == 'sco') {
  186. /*
  187. * This is a specific implementation for SCORM 1.2, matching page 26 of SCORM 1.2's RTE
  188. * "Normally the SCO determines its own status and passes it to the LMS.
  189. * 1) If cmi.core.credit is set to "credit" and there is a mastery
  190. * score in the manifest (adlcp:masteryscore), the LMS can change
  191. * the status to either passed or failed depending on the
  192. * student's score compared to the mastery score.
  193. * 2) If there is no mastery score in the manifest
  194. * (adlcp:masteryscore), the LMS cannot override SCO
  195. * determined status.
  196. * 3) If the student is taking the SCO for no-credit, there is no
  197. * change to the lesson_status, with one exception. If the
  198. * lesson_mode is "browse", the lesson_status may change to
  199. * "browsed" even if the cmi.core.credit is set to no-credit.
  200. * "
  201. * Additionally, the LMS behaviour should be:
  202. * If a SCO sets the cmi.core.lesson_status then there is no problem.
  203. * However, the SCORM does not force the SCO to set the cmi.core.lesson_status.
  204. * There is some additional requirements that must be adhered to
  205. * successfully handle these cases:
  206. * Upon initial launch
  207. * the LMS should set the cmi.core.lesson_status to "not attempted".
  208. * Upon receiving the LMSFinish() call or the user navigates away,
  209. * the LMS should set the cmi.core.lesson_status for the SCO to "completed".
  210. * After setting the cmi.core.lesson_status to "completed",
  211. * the LMS should now check to see if a Mastery Score has been
  212. * specified in the cmi.student_data.mastery_score, if supported,
  213. * or the manifest that the SCO is a member of.
  214. * If a Mastery Score is provided and the SCO did set the
  215. * cmi.core.score.raw, the LMS shall compare the cmi.core.score.raw
  216. * to the Mastery Score and set the cmi.core.lesson_status to
  217. * either "passed" or "failed". If no Mastery Score is provided,
  218. * the LMS will leave the cmi.core.lesson_status as "completed"
  219. */
  220. $masteryScore = $myLPI->get_mastery_score();
  221. if ($masteryScore == -1 || empty($masteryScore)) {
  222. $masteryScore = false;
  223. }
  224. $credit = $myLPI->get_credit();
  225. /**
  226. * 1) If cmi.core.credit is set to "credit" and there is a mastery
  227. * score in the manifest (adlcp:masteryscore), the LMS can change
  228. * the status to either passed or failed depending on the
  229. * student's score compared to the mastery score.
  230. */
  231. if ($credit == 'credit' &&
  232. $masteryScore &&
  233. (isset($score) && $score != -1) &&
  234. !$statusIsSet && !$statusSignalReceived
  235. ) {
  236. if ($score >= $masteryScore) {
  237. $myLPI->set_status('passed');
  238. if ($debug) {
  239. error_log('Set status: passed');
  240. }
  241. } else {
  242. $myLPI->set_status('failed');
  243. if ($debug) {
  244. error_log('Set status: failed');
  245. }
  246. }
  247. $statusIsSet = true;
  248. }
  249. /**
  250. * 2) If there is no mastery score in the manifest
  251. * (adlcp:masteryscore), the LMS cannot override SCO
  252. * determined status.
  253. */
  254. if (!$statusIsSet && !$masteryScore && !$statusSignalReceived) {
  255. if (!empty($status)) {
  256. if ($debug) {
  257. error_log("Set status: $status because: statusSignalReceived ");
  258. }
  259. $myLPI->set_status($status);
  260. $statusIsSet = true;
  261. }
  262. //if no status was set directly, we keep the previous one
  263. }
  264. /**
  265. * 3) If the student is taking the SCO for no-credit, there is no
  266. * change to the lesson_status, with one exception. If the
  267. * lesson_mode is "browse", the lesson_status may change to
  268. * "browsed" even if the cmi.core.credit is set to no-credit.
  269. */
  270. if (!$statusIsSet && $credit == 'no-credit' && !$statusSignalReceived) {
  271. $mode = $myLPI->get_lesson_mode();
  272. if ($mode == 'browse' && $status == 'browsed') {
  273. if ($debug) {
  274. error_log("Set status: $status because mode browse");
  275. }
  276. $myLPI->set_status($status);
  277. $statusIsSet = true;
  278. }
  279. //if no status was set directly, we keep the previous one
  280. }
  281. /**
  282. * If a SCO sets the cmi.core.lesson_status then there is no problem.
  283. * However, the SCORM does not force the SCO to set the
  284. * cmi.core.lesson_status. There is some additional requirements
  285. * that must be adhered to successfully handle these cases:.
  286. */
  287. if (!$statusIsSet && empty($status) && !$statusSignalReceived) {
  288. /**
  289. * Upon initial launch the LMS should set the
  290. * cmi.core.lesson_status to "not attempted".
  291. */
  292. // this case should be handled by LMSInitialize() and xajax_switch_item()
  293. /**
  294. * Upon receiving the LMSFinish() call or the user navigates
  295. * away, the LMS should set the cmi.core.lesson_status for the
  296. * SCO to "completed".
  297. */
  298. if ($lmsFinish || $userNavigatesAway) {
  299. $myStatus = 'completed';
  300. /**
  301. * After setting the cmi.core.lesson_status to "completed",
  302. * the LMS should now check to see if a Mastery Score has been
  303. * specified in the cmi.student_data.mastery_score, if supported,
  304. * or the manifest that the SCO is a member of.
  305. * If a Mastery Score is provided and the SCO did set the
  306. * cmi.core.score.raw, the LMS shall compare the cmi.core.score.raw
  307. * to the Mastery Score and set the cmi.core.lesson_status to
  308. * either "passed" or "failed". If no Mastery Score is provided,
  309. * the LMS will leave the cmi.core.lesson_status as "completed”.
  310. */
  311. if ($masteryScore && (isset($score) && $score != -1)) {
  312. if ($score >= $masteryScore) {
  313. $myStatus = 'passed';
  314. } else {
  315. $myStatus = 'failed';
  316. }
  317. }
  318. if ($debug) {
  319. error_log("Set status: $myStatus because lmsFinish || userNavigatesAway");
  320. }
  321. $myLPI->set_status($myStatus);
  322. $statusIsSet = true;
  323. }
  324. }
  325. // End of type=='sco'
  326. }
  327. // If no previous condition changed the SCO status, proceed with a
  328. // generic behaviour
  329. if (!$statusIsSet && !$statusSignalReceived) {
  330. // Default behaviour
  331. if (isset($status) && $status != '' && $status != 'undefined') {
  332. if ($debug > 1) {
  333. error_log('Calling set_status('.$status.')');
  334. }
  335. $myLPI->set_status($status);
  336. if ($debug > 1) {
  337. error_log('Done calling set_status: checking from memory: '.$myLPI->get_status(false));
  338. }
  339. } else {
  340. if ($debug > 1) {
  341. error_log("Status not updated");
  342. }
  343. }
  344. }
  345. if (isset($time) && $time != '' && $time != 'undefined') {
  346. // If big integer, then it's a timestamp, otherwise it's normal scorm time.
  347. if ($debug > 1) {
  348. error_log('Calling set_time('.$time.') ');
  349. }
  350. if ($time == intval(strval($time)) && $time > 1000000) {
  351. if ($debug > 1) {
  352. error_log("Time is INT");
  353. }
  354. $real_time = time() - $time;
  355. if ($debug > 1) {
  356. error_log('Calling $real_time '.$real_time.' ');
  357. }
  358. $myLPI->set_time($real_time, 'int');
  359. } else {
  360. if ($debug > 1) {
  361. error_log("Time is in SCORM format");
  362. }
  363. if ($debug > 1) {
  364. error_log('Calling $time '.$time.' ');
  365. }
  366. $myLPI->set_time($time, 'scorm');
  367. }
  368. } else {
  369. $myLPI->current_stop_time = time();
  370. }
  371. if (isset($suspend) && $suspend != '' && $suspend != 'undefined') {
  372. $myLPI->current_data = $suspend;
  373. }
  374. if (isset($location) && $location != '' && $location != 'undefined') {
  375. $myLPI->set_lesson_location($location);
  376. }
  377. // Deal with interactions provided in arrays in the following format:
  378. // id(0), type(1), time(2), weighting(3), correct_responses(4), student_response(5), result(6), latency(7)
  379. if (is_array($interactions) && count($interactions) > 0) {
  380. foreach ($interactions as $index => $interaction) {
  381. //$mylpi->add_interaction($index,$interactions[$index]);
  382. //fix DT#4444
  383. $clean_interaction = str_replace('@.|@', ',', $interactions[$index]);
  384. $myLPI->add_interaction($index, $clean_interaction);
  385. }
  386. }
  387. if ($core_exit != 'undefined') {
  388. $myLPI->set_core_exit($core_exit);
  389. }
  390. $myLP->save_item($item_id, false);
  391. }
  392. $myStatusInDB = $myLPI->get_status(true);
  393. if ($debug) {
  394. error_log("Status in DB: $myStatusInDB");
  395. }
  396. if ($myStatusInDB != 'completed' &&
  397. $myStatusInDB != 'passed' &&
  398. $myStatusInDB != 'browsed' &&
  399. $myStatusInDB != 'failed'
  400. ) {
  401. $myStatusInMemory = $myLPI->get_status(false);
  402. if ($debug) {
  403. error_log("myStatusInMemory: $myStatusInMemory");
  404. }
  405. if ($myStatusInMemory != $myStatusInDB) {
  406. $myStatus = $myStatusInMemory;
  407. } else {
  408. $myStatus = $myStatusInDB;
  409. }
  410. } else {
  411. $myStatus = $myStatusInDB;
  412. }
  413. $myTotal = $myLP->getTotalItemsCountWithoutDirs();
  414. $myComplete = $myLP->get_complete_items_count();
  415. $myProgressMode = $myLP->get_progress_bar_mode();
  416. $myProgressMode = $myProgressMode == '' ? '%' : $myProgressMode;
  417. if ($debug > 1) {
  418. error_log("mystatus: $myStatus");
  419. error_log("myprogress_mode: $myProgressMode");
  420. error_log("progress: $myComplete / $myTotal");
  421. }
  422. if ($myLPI->get_type() != 'sco') {
  423. // If this object's JS status has not been updated by the SCORM API, update now.
  424. $return .= "olms.lesson_status='".$myStatus."';";
  425. }
  426. $return .= "update_toc('".$myStatus."','".$item_id."');";
  427. $update_list = $myLP->get_update_queue();
  428. foreach ($update_list as $my_upd_id => $my_upd_status) {
  429. if ($my_upd_id != $item_id) {
  430. /* Only update the status from other items (i.e. parents and brothers),
  431. do not update current as we just did it already. */
  432. $return .= "update_toc('".$my_upd_status."','".$my_upd_id."');";
  433. }
  434. }
  435. $return .= "update_progress_bar('$myComplete', '$myTotal', '$myProgressMode');";
  436. if (!Session::read('login_as')) {
  437. // If $_SESSION['login_as'] is set, then the user is an admin logged as the user.
  438. $tbl_track_login = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
  439. $sql = "SELECT login_id, login_date
  440. FROM $tbl_track_login
  441. WHERE login_user_id= ".api_get_user_id()."
  442. ORDER BY login_date DESC
  443. LIMIT 0,1";
  444. $q_last_connection = Database::query($sql);
  445. if (Database::num_rows($q_last_connection) > 0) {
  446. $current_time = api_get_utc_datetime();
  447. $row = Database::fetch_array($q_last_connection);
  448. $i_id_last_connection = $row['login_id'];
  449. $sql = "UPDATE $tbl_track_login
  450. SET logout_date='".$current_time."'
  451. WHERE login_id = $i_id_last_connection";
  452. Database::query($sql);
  453. }
  454. }
  455. if ($myLP->get_type() == 2) {
  456. $return .= 'update_stats();';
  457. }
  458. // To be sure progress is updated.
  459. $myLP->save_last();
  460. Session::write('lpobject', serialize($myLP));
  461. Session::write('oLP', $myLP);
  462. if ($debug > 0) {
  463. error_log("lp_view_session_id :".$myLP->lp_view_session_id);
  464. error_log('---------------- lp_ajax_save_item.php : save_item end ----- ');
  465. }
  466. $logInfo = [
  467. 'tool' => TOOL_LEARNPATH,
  468. 'tool_id' => $myLP->get_id(),
  469. 'action_details' => $myLP->getCurrentAttempt(),
  470. 'tool_id_detail' => $myLP->get_current_item_id(),
  471. 'action' => 'save_item',
  472. ];
  473. Event::registerLog($logInfo);
  474. return $return;
  475. }
  476. $interactions = [];
  477. if (isset($_REQUEST['interact'])) {
  478. if (is_array($_REQUEST['interact'])) {
  479. foreach ($_REQUEST['interact'] as $idx => $interac) {
  480. $interactions[$idx] = preg_split('/,/', substr($interac, 1, -1));
  481. if (!isset($interactions[$idx][7])) { // Make sure there are 7 elements.
  482. $interactions[$idx][7] = '';
  483. }
  484. }
  485. }
  486. }
  487. echo save_item(
  488. (!empty($_REQUEST['lid']) ? $_REQUEST['lid'] : null),
  489. (!empty($_REQUEST['uid']) ? $_REQUEST['uid'] : null),
  490. (!empty($_REQUEST['vid']) ? $_REQUEST['vid'] : null),
  491. (!empty($_REQUEST['iid']) ? $_REQUEST['iid'] : null),
  492. (!empty($_REQUEST['s']) ? $_REQUEST['s'] : null),
  493. (!empty($_REQUEST['max']) ? $_REQUEST['max'] : null),
  494. (!empty($_REQUEST['min']) ? $_REQUEST['min'] : null),
  495. (!empty($_REQUEST['status']) ? $_REQUEST['status'] : null),
  496. (!empty($_REQUEST['t']) ? $_REQUEST['t'] : null),
  497. (!empty($_REQUEST['suspend']) ? $_REQUEST['suspend'] : null),
  498. (!empty($_REQUEST['loc']) ? $_REQUEST['loc'] : null),
  499. $interactions,
  500. (!empty($_REQUEST['core_exit']) ? $_REQUEST['core_exit'] : ''),
  501. (!empty($_REQUEST['session_id']) ? $_REQUEST['session_id'] : ''),
  502. (!empty($_REQUEST['course_id']) ? $_REQUEST['course_id'] : ''),
  503. (empty($_REQUEST['finish']) ? 0 : 1),
  504. (empty($_REQUEST['userNavigatesAway']) ? 0 : 1),
  505. (empty($_REQUEST['statusSignalReceived']) ? 0 : 1)
  506. );