lp_ajax_save_item.php 21 KB

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