lp_ajax_save_item.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  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. if (isset($max) && $max != -1) {
  108. $myLPI->max_score = $max;
  109. $myLPI->set_max_score($max);
  110. if ($debug > 1) {
  111. error_log("Setting max_score: $max");
  112. }
  113. }
  114. if (isset($min) && $min != -1 && $min != 'undefined') {
  115. $myLPI->min_score = $min;
  116. if ($debug > 1) {
  117. error_log("Setting min_score: $min");
  118. }
  119. }
  120. // set_score function used to save the status, but this is not the case anymore
  121. if (isset($score) && $score != -1) {
  122. if ($debug > 1) {
  123. error_log('Calling set_score('.$score.')');
  124. error_log('set_score changes the status to failed/passed if mastery score is provided');
  125. }
  126. $myLPI->set_score($score);
  127. if ($debug > 1) {
  128. error_log('Done calling set_score '.$myLPI->get_score());
  129. }
  130. } else {
  131. if ($debug > 1) {
  132. error_log('Score not updated');
  133. }
  134. }
  135. $statusIsSet = false;
  136. // Default behaviour.
  137. if (isset($status) && $status != '' && $status != 'undefined') {
  138. if ($debug > 1) {
  139. error_log('Calling set_status('.$status.')');
  140. }
  141. $myLPI->set_status($status);
  142. $statusIsSet = true;
  143. if ($debug > 1) {
  144. error_log('Done calling set_status: checking from memory: '.$myLPI->get_status(false));
  145. }
  146. } else {
  147. if ($debug > 1) {
  148. error_log('Status not updated');
  149. }
  150. }
  151. $my_type = $myLPI->get_type();
  152. // Set status to completed for hotpotatoes if score > 80%.
  153. if ($my_type == 'hotpotatoes') {
  154. if ((empty($status) || $status == 'undefined' || $status == 'not attempted') && $max > 0) {
  155. if (($score / $max) > 0.8) {
  156. $myStatus = 'completed';
  157. if ($debug > 1) {
  158. error_log('Calling set_status('.$myStatus.') for hotpotatoes');
  159. }
  160. $myLPI->set_status($myStatus);
  161. $statusIsSet = true;
  162. if ($debug > 1) {
  163. error_log('Done calling set_status for hotpotatoes - now '.$myLPI->get_status(false));
  164. }
  165. }
  166. } elseif ($status == 'completed' && $max > 0 && ($score / $max) < 0.8) {
  167. $myStatus = 'failed';
  168. if ($debug > 1) {
  169. error_log('Calling set_status('.$myStatus.') for hotpotatoes');
  170. }
  171. $myLPI->set_status($myStatus);
  172. $statusIsSet = true;
  173. if ($debug > 1) {
  174. error_log('Done calling set_status for hotpotatoes - now '.$myLPI->get_status(false));
  175. }
  176. }
  177. } elseif ($my_type == 'sco') {
  178. /*
  179. * This is a specific implementation for SCORM 1.2, matching page 26 of SCORM 1.2's RTE
  180. * "Normally the SCO determines its own status and passes it to the LMS.
  181. * 1) If cmi.core.credit is set to "credit" and there is a mastery
  182. * score in the manifest (adlcp:masteryscore), the LMS can change
  183. * the status to either passed or failed depending on the
  184. * student's score compared to the mastery score.
  185. * 2) If there is no mastery score in the manifest
  186. * (adlcp:masteryscore), the LMS cannot override SCO
  187. * determined status.
  188. * 3) If the student is taking the SCO for no-credit, there is no
  189. * change to the lesson_status, with one exception. If the
  190. * lesson_mode is "browse", the lesson_status may change to
  191. * "browsed" even if the cmi.core.credit is set to no-credit.
  192. * "
  193. * Additionally, the LMS behaviour should be:
  194. * If a SCO sets the cmi.core.lesson_status then there is no problem.
  195. * However, the SCORM does not force the SCO to set the cmi.core.lesson_status.
  196. * There is some additional requirements that must be adhered to
  197. * successfully handle these cases:
  198. * Upon initial launch
  199. * the LMS should set the cmi.core.lesson_status to "not attempted".
  200. * Upon receiving the LMSFinish() call or the user navigates away,
  201. * the LMS should set the cmi.core.lesson_status for the SCO to "completed".
  202. * After setting the cmi.core.lesson_status to "completed",
  203. * the LMS should now check to see if a Mastery Score has been
  204. * specified in the cmi.student_data.mastery_score, if supported,
  205. * or the manifest that the SCO is a member of.
  206. * If a Mastery Score is provided and the SCO did set the
  207. * cmi.core.score.raw, the LMS shall compare the cmi.core.score.raw
  208. * to the Mastery Score and set the cmi.core.lesson_status to
  209. * either "passed" or "failed". If no Mastery Score is provided,
  210. * the LMS will leave the cmi.core.lesson_status as "completed"
  211. */
  212. $masteryScore = $myLPI->get_mastery_score();
  213. if ($masteryScore == -1 || empty($masteryScore)) {
  214. $masteryScore = false;
  215. }
  216. $credit = $myLPI->get_credit();
  217. /**
  218. * 1) If cmi.core.credit is set to "credit" and there is a mastery
  219. * score in the manifest (adlcp:masteryscore), the LMS can change
  220. * the status to either passed or failed depending on the
  221. * student's score compared to the mastery score.
  222. */
  223. if ($credit == 'credit' &&
  224. $masteryScore &&
  225. (isset($score) && $score != -1) &&
  226. !$statusIsSet && !$statusSignalReceived
  227. ) {
  228. if ($score >= $masteryScore) {
  229. $myLPI->set_status('passed');
  230. if ($debug) {
  231. error_log('Set status: passed');
  232. }
  233. } else {
  234. $myLPI->set_status('failed');
  235. if ($debug) {
  236. error_log('Set status: failed');
  237. }
  238. }
  239. $statusIsSet = true;
  240. }
  241. /**
  242. * 2) If there is no mastery score in the manifest
  243. * (adlcp:masteryscore), the LMS cannot override SCO
  244. * determined status.
  245. */
  246. if (!$statusIsSet && !$masteryScore && !$statusSignalReceived) {
  247. if (!empty($status)) {
  248. if ($debug) {
  249. error_log("Set status: $status because: statusSignalReceived ");
  250. }
  251. $myLPI->set_status($status);
  252. $statusIsSet = true;
  253. }
  254. //if no status was set directly, we keep the previous one
  255. }
  256. /**
  257. * 3) If the student is taking the SCO for no-credit, there is no
  258. * change to the lesson_status, with one exception. If the
  259. * lesson_mode is "browse", the lesson_status may change to
  260. * "browsed" even if the cmi.core.credit is set to no-credit.
  261. */
  262. if (!$statusIsSet && $credit == 'no-credit' && !$statusSignalReceived) {
  263. $mode = $myLPI->get_lesson_mode();
  264. if ($mode == 'browse' && $status == 'browsed') {
  265. if ($debug) {
  266. error_log("Set status: $status because mode browse");
  267. }
  268. $myLPI->set_status($status);
  269. $statusIsSet = true;
  270. }
  271. //if no status was set directly, we keep the previous one
  272. }
  273. /**
  274. * If a SCO sets the cmi.core.lesson_status then there is no problem.
  275. * However, the SCORM does not force the SCO to set the
  276. * cmi.core.lesson_status. There is some additional requirements
  277. * that must be adhered to successfully handle these cases:.
  278. */
  279. if (!$statusIsSet && empty($status) && !$statusSignalReceived) {
  280. /**
  281. * Upon initial launch the LMS should set the
  282. * cmi.core.lesson_status to "not attempted".
  283. */
  284. // this case should be handled by LMSInitialize() and xajax_switch_item()
  285. /**
  286. * Upon receiving the LMSFinish() call or the user navigates
  287. * away, the LMS should set the cmi.core.lesson_status for the
  288. * SCO to "completed".
  289. */
  290. if ($lmsFinish || $userNavigatesAway) {
  291. $myStatus = 'completed';
  292. /**
  293. * After setting the cmi.core.lesson_status to "completed",
  294. * the LMS should now check to see if a Mastery Score has been
  295. * specified in the cmi.student_data.mastery_score, if supported,
  296. * or the manifest that the SCO is a member of.
  297. * If a Mastery Score is provided and the SCO did set the
  298. * cmi.core.score.raw, the LMS shall compare the cmi.core.score.raw
  299. * to the Mastery Score and set the cmi.core.lesson_status to
  300. * either "passed" or "failed". If no Mastery Score is provided,
  301. * the LMS will leave the cmi.core.lesson_status as "completed”.
  302. */
  303. if ($masteryScore && (isset($score) && $score != -1)) {
  304. if ($score >= $masteryScore) {
  305. $myStatus = 'passed';
  306. } else {
  307. $myStatus = 'failed';
  308. }
  309. }
  310. if ($debug) {
  311. error_log("Set status: $myStatus because lmsFinish || userNavigatesAway");
  312. }
  313. $myLPI->set_status($myStatus);
  314. $statusIsSet = true;
  315. }
  316. }
  317. // End of type=='sco'
  318. }
  319. // If no previous condition changed the SCO status, proceed with a
  320. // generic behaviour
  321. if (!$statusIsSet && !$statusSignalReceived) {
  322. // Default behaviour
  323. if (isset($status) && $status != '' && $status != 'undefined') {
  324. if ($debug > 1) {
  325. error_log('Calling set_status('.$status.')');
  326. }
  327. $myLPI->set_status($status);
  328. if ($debug > 1) {
  329. error_log('Done calling set_status: checking from memory: '.$myLPI->get_status(false));
  330. }
  331. } else {
  332. if ($debug > 1) {
  333. error_log("Status not updated");
  334. }
  335. }
  336. }
  337. if (isset($time) && $time != '' && $time != 'undefined') {
  338. // If big integer, then it's a timestamp, otherwise it's normal scorm time.
  339. if ($debug > 1) {
  340. error_log('Calling set_time('.$time.') ');
  341. }
  342. if ($time == intval(strval($time)) && $time > 1000000) {
  343. if ($debug > 1) {
  344. error_log("Time is INT");
  345. }
  346. $real_time = time() - $time;
  347. if ($debug > 1) {
  348. error_log('Calling $real_time '.$real_time.' ');
  349. }
  350. $myLPI->set_time($real_time, 'int');
  351. } else {
  352. if ($debug > 1) {
  353. error_log("Time is in SCORM format");
  354. }
  355. if ($debug > 1) {
  356. error_log('Calling $time '.$time.' ');
  357. }
  358. $myLPI->set_time($time, 'scorm');
  359. }
  360. } else {
  361. $myLPI->current_stop_time = time();
  362. }
  363. if (isset($suspend) && $suspend != '' && $suspend != 'undefined') {
  364. $myLPI->current_data = $suspend;
  365. }
  366. if (isset($location) && $location != '' && $location != 'undefined') {
  367. $myLPI->set_lesson_location($location);
  368. }
  369. // Deal with interactions provided in arrays in the following format:
  370. // id(0), type(1), time(2), weighting(3), correct_responses(4), student_response(5), result(6), latency(7)
  371. if (is_array($interactions) && count($interactions) > 0) {
  372. foreach ($interactions as $index => $interaction) {
  373. //$mylpi->add_interaction($index,$interactions[$index]);
  374. //fix DT#4444
  375. $clean_interaction = str_replace('@.|@', ',', $interactions[$index]);
  376. $myLPI->add_interaction($index, $clean_interaction);
  377. }
  378. }
  379. if ($core_exit != 'undefined') {
  380. $myLPI->set_core_exit($core_exit);
  381. }
  382. $myLP->save_item($item_id, false);
  383. }
  384. $myStatusInDB = $myLPI->get_status(true);
  385. if ($debug) {
  386. error_log("Status in DB: $myStatusInDB");
  387. }
  388. if ($myStatusInDB != 'completed' &&
  389. $myStatusInDB != 'passed' &&
  390. $myStatusInDB != 'browsed' &&
  391. $myStatusInDB != 'failed'
  392. ) {
  393. $myStatusInMemory = $myLPI->get_status(false);
  394. if ($debug) {
  395. error_log("myStatusInMemory: $myStatusInMemory");
  396. }
  397. if ($myStatusInMemory != $myStatusInDB) {
  398. $myStatus = $myStatusInMemory;
  399. } else {
  400. $myStatus = $myStatusInDB;
  401. }
  402. } else {
  403. $myStatus = $myStatusInDB;
  404. }
  405. $myTotal = $myLP->getTotalItemsCountWithoutDirs();
  406. $myComplete = $myLP->get_complete_items_count();
  407. $myProgressMode = $myLP->get_progress_bar_mode();
  408. $myProgressMode = $myProgressMode == '' ? '%' : $myProgressMode;
  409. if ($debug > 1) {
  410. error_log("mystatus: $myStatus");
  411. error_log("myprogress_mode: $myProgressMode");
  412. error_log("progress: $myComplete / $myTotal");
  413. }
  414. if ($myLPI->get_type() != 'sco') {
  415. // If this object's JS status has not been updated by the SCORM API, update now.
  416. $return .= "olms.lesson_status='".$myStatus."';";
  417. }
  418. $return .= "update_toc('".$myStatus."','".$item_id."');";
  419. $update_list = $myLP->get_update_queue();
  420. foreach ($update_list as $my_upd_id => $my_upd_status) {
  421. if ($my_upd_id != $item_id) {
  422. /* Only update the status from other items (i.e. parents and brothers),
  423. do not update current as we just did it already. */
  424. $return .= "update_toc('".$my_upd_status."','".$my_upd_id."');";
  425. }
  426. }
  427. $return .= "update_progress_bar('$myComplete', '$myTotal', '$myProgressMode');";
  428. if (!Session::read('login_as')) {
  429. // If $_SESSION['login_as'] is set, then the user is an admin logged as the user.
  430. $tbl_track_login = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
  431. $sql = "SELECT login_id, login_date
  432. FROM $tbl_track_login
  433. WHERE login_user_id= ".api_get_user_id()."
  434. ORDER BY login_date DESC
  435. LIMIT 0,1";
  436. $q_last_connection = Database::query($sql);
  437. if (Database::num_rows($q_last_connection) > 0) {
  438. $current_time = api_get_utc_datetime();
  439. $row = Database::fetch_array($q_last_connection);
  440. $i_id_last_connection = $row['login_id'];
  441. $sql = "UPDATE $tbl_track_login
  442. SET logout_date='".$current_time."'
  443. WHERE login_id = $i_id_last_connection";
  444. Database::query($sql);
  445. }
  446. }
  447. if ($myLP->get_type() == 2) {
  448. $return .= "update_stats();";
  449. }
  450. $logInfo = [
  451. 'tool' => TOOL_LEARNPATH,
  452. 'tool_id' => $myLP->get_id(),
  453. 'tool_id_detail' => $myLP->get_current_item_id(),
  454. 'action' => 'save_item',
  455. 'info' => '',
  456. ];
  457. Event::registerLog($logInfo);
  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. return $return;
  467. }
  468. $interactions = [];
  469. if (isset($_REQUEST['interact'])) {
  470. if (is_array($_REQUEST['interact'])) {
  471. foreach ($_REQUEST['interact'] as $idx => $interac) {
  472. $interactions[$idx] = preg_split('/,/', substr($interac, 1, -1));
  473. if (!isset($interactions[$idx][7])) { // Make sure there are 7 elements.
  474. $interactions[$idx][7] = '';
  475. }
  476. }
  477. }
  478. }
  479. echo save_item(
  480. (!empty($_REQUEST['lid']) ? $_REQUEST['lid'] : null),
  481. (!empty($_REQUEST['uid']) ? $_REQUEST['uid'] : null),
  482. (!empty($_REQUEST['vid']) ? $_REQUEST['vid'] : null),
  483. (!empty($_REQUEST['iid']) ? $_REQUEST['iid'] : null),
  484. (!empty($_REQUEST['s']) ? $_REQUEST['s'] : null),
  485. (!empty($_REQUEST['max']) ? $_REQUEST['max'] : null),
  486. (!empty($_REQUEST['min']) ? $_REQUEST['min'] : null),
  487. (!empty($_REQUEST['status']) ? $_REQUEST['status'] : null),
  488. (!empty($_REQUEST['t']) ? $_REQUEST['t'] : null),
  489. (!empty($_REQUEST['suspend']) ? $_REQUEST['suspend'] : null),
  490. (!empty($_REQUEST['loc']) ? $_REQUEST['loc'] : null),
  491. $interactions,
  492. (!empty($_REQUEST['core_exit']) ? $_REQUEST['core_exit'] : ''),
  493. (!empty($_REQUEST['session_id']) ? $_REQUEST['session_id'] : ''),
  494. (!empty($_REQUEST['course_id']) ? $_REQUEST['course_id'] : ''),
  495. (empty($_REQUEST['finish']) ? 0 : 1),
  496. (empty($_REQUEST['userNavigatesAway']) ? 0 : 1),
  497. (empty($_REQUEST['statusSignalReceived']) ? 0 : 1)
  498. );