Upgrade 1-11.38

This commit is contained in:
xesmyd
2026-03-30 14:10:30 +02:00
parent f2a7e6d1fc
commit ac648ef29d
24665 changed files with 69682 additions and 2205004 deletions
+2
View File
@@ -38,6 +38,8 @@ use ChamiloSession as Session;
/* INIT SECTION */
die('DEPRECATED');
$debug = 0;
// Flag to allow for anonymous user - needs to be set before global.inc.php.
+38 -25
View File
@@ -266,6 +266,8 @@ class learnpath
$lp_item_id_list[] = $row['iid'];
switch ($this->type) {
case 3: //aicc
// @deprecated AICC support (lp_type=3) is deprecated and no longer executed.
/*
$oItem = new aiccItem('db', $row['iid'], $course_id);
if (is_object($oItem)) {
$my_item_id = $oItem->get_id();
@@ -283,6 +285,7 @@ class learnpath
);
}
}
*/
break;
case 2:
$oItem = new scormItem('db', $row['iid'], $course_id);
@@ -807,6 +810,7 @@ class learnpath
$type = 1;
break;
case 'aicc':
// @deprecated AICC support (lp_type=3) is deprecated and no longer executed.
break;
}
@@ -1077,7 +1081,8 @@ class learnpath
self::toggle_publish($this->lp_id, 'i');
if ($this->type == 2 || $this->type == 3) {
// @deprecated AICC support (lp_type=3) is deprecated, only SCORM (type=2) deletes files.
if ($this->type == 2 /* || $this->type == 3*/) {
// This is a scorm learning path, delete the files as well.
$sql = "SELECT path FROM $lp
WHERE iid = ".$this->lp_id;
@@ -3928,6 +3933,8 @@ class learnpath
}
break;
case 3:
// @deprecated AICC support (lp_type=3) is deprecated and no longer executed.
/*
if ($this->debug > 2) {
error_log('In learnpath::get_link() '.__LINE__.' - Item type: '.$lp_item_type, 0);
}
@@ -3950,11 +3957,9 @@ class learnpath
// Distant url, return as is.
$file = $lp_item_path;
// Enabled and modified by Ivan Tcholakov, 16-OCT-2008.
/*
if (stristr($file,'<servername>') !== false) {
$file = str_replace('<servername>', $course_path.'/scorm/'.$lp_path.'/', $lp_item_path);
}
*/
//if (stristr($file,'<servername>') !== false) {
// $file = str_replace('<servername>', $course_path.'/scorm/'.$lp_path.'/', $lp_item_path);
//}
if (stripos($file, '<servername>') !== false) {
//$file = str_replace('<servername>',$course_path.'/scorm/'.$lp_path.'/',$lp_item_path);
$web_course_path = str_replace('https://', '', str_replace('http://', '', $course_path));
@@ -3979,6 +3984,7 @@ class learnpath
} else {
$file = 'lp_content.php?type=dir&'.api_get_cidreq();
}
*/
break;
case 4:
break;
@@ -5806,8 +5812,9 @@ class learnpath
if ($this->current != 0 && isset($this->items[$this->current]) && is_object($this->items[$this->current])) {
$type = $this->get_type();
$item_type = $this->items[$this->current]->get_type();
// @deprecated AICC support (lp_type=3) is deprecated, condition removed.
if (($type == 2 && $item_type != 'sco') ||
($type == 3 && $item_type != 'au') ||
// ($type == 3 && $item_type != 'au') ||
(
$type == 1 && $item_type != TOOL_QUIZ && $item_type != TOOL_HOTPOTATOES &&
WhispeakAuthPlugin::isAllowedToSaveLpItem($this->current)
@@ -5863,6 +5870,8 @@ class learnpath
}
switch ($this->get_type()) {
case '3':
// @deprecated AICC support (lp_type=3) is deprecated and no longer executed.
/*
if ($this->items[$this->last]->get_type() != 'au') {
if ($debug) {
error_log('In learnpath::stop_previous_item() - '.$this->last.' in lp_type 3 is <> au');
@@ -5873,6 +5882,7 @@ class learnpath
error_log('In learnpath::stop_previous_item() - Item is an AU, saving is managed by AICC signals');
}
}
*/
break;
case '2':
if ($this->items[$this->last]->get_type() != 'sco') {
@@ -6940,7 +6950,8 @@ class learnpath
$isConfigPage = false,
$allowExpand = true,
$action = '',
$extraField = []
$extraField = [],
$noEdition = false
) {
$actionsRight = '';
$lpId = $this->lp_id;
@@ -6981,23 +6992,25 @@ class learnpath
])
);
$actionsLeft .= Display::url(
Display::return_icon(
'upload_audio.png',
get_lang('UpdateAllAudioFragments'),
'',
ICON_SIZE_MEDIUM
),
'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
'action' => 'admin_view',
'lp_id' => $lpId,
'updateaudio' => 'true',
])
);
if (!$noEdition) {
$actionsLeft .= Display::url(
Display::return_icon(
'upload_audio.png',
get_lang('UpdateAllAudioFragments'),
'',
ICON_SIZE_MEDIUM
),
'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
'action' => 'admin_view',
'lp_id' => $lpId,
'updateaudio' => 'true',
])
);
}
$subscriptionSettings = self::getSubscriptionSettings();
$request = api_request_uri();
if (strpos($request, 'edit') === false) {
if ((strpos($request, 'edit') === false) && !$noEdition) {
$actionsLeft .= Display::url(
Display::return_icon(
'settings.png',
@@ -7012,7 +7025,7 @@ class learnpath
);
}
if ((strpos($request, 'build') === false &&
if ((strpos($request, 'build') === false && !$noEdition &&
strpos($request, 'add_item') === false) ||
in_array($action, ['add_audio'])
) {
@@ -13329,7 +13342,7 @@ EOD;
/**
* Get the item of exercise type (evaluation type).
*
* @return array The final evaluation. Otherwise return false
* @return learnpathItem The final evaluation. Otherwise return false
*/
public function getFinalEvaluationItem()
{
@@ -13342,7 +13355,7 @@ EOD;
$exercises[] = $item;
}
return array_pop($exercises);
return end($exercises);
}
/**
+58 -57
View File
@@ -2366,61 +2366,65 @@ class learnpathItem
// For one and first attempt.
if ($this->prevent_reinit == 1) {
// 2. If is completed we check the results in the DB of the quiz.
if ($returnstatus) {
$checkLastScoreAttempt = api_get_configuration_value('lp_prerequisite_use_last_attempt_only');
$orderBy = ($checkLastScoreAttempt ? 'ORDER BY exe_date DESC' : 'ORDER BY (exe_result/exe_weighting) DESC');
$sql = 'SELECT exe_result, exe_weighting
FROM '.Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES).'
WHERE
exe_exo_id = '.$items[$refs_list[$prereqs_string]]->path.' AND
exe_user_id = '.$user_id.' AND
orig_lp_id = '.$this->lp_id.' AND
orig_lp_item_id = '.$prereqs_string.' AND
status <> "incomplete" AND
c_id = '.$courseId.'
'.$orderBy.'
LIMIT 0, 1';
$rs_quiz = Database::query($sql);
if ($quiz = Database::fetch_array($rs_quiz)) {
/** @var learnpathItem $myItemToCheck */
$myItemToCheck = $items[$refs_list[$this->get_id()]];
$minScore = $myItemToCheck->getPrerequisiteMinScore();
$maxScore = $myItemToCheck->getPrerequisiteMaxScore();
// 2. Always check the results in the DB of the quiz (previously checked only if status was completed)
$checkLastScoreAttempt = api_get_configuration_value('lp_prerequisite_use_last_attempt_only');
$orderBy = ($checkLastScoreAttempt ? 'ORDER BY exe_date DESC' : 'ORDER BY (exe_result/exe_weighting) DESC');
$sql = 'SELECT exe_result, exe_weighting
FROM '.Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES).'
WHERE
exe_exo_id = '.$items[$refs_list[$prereqs_string]]->path.' AND
exe_user_id = '.$user_id.' AND
orig_lp_id = '.$this->lp_id.' AND
orig_lp_item_id = '.$prereqs_string.' AND
status <> "incomplete" AND
c_id = '.$courseId.'
'.$orderBy.'
LIMIT 0, 1';
$rs_quiz = Database::query($sql);
if ($quiz = Database::fetch_array($rs_quiz)) {
/** @var learnpathItem $myItemToCheck */
$myItemToCheck = $items[$refs_list[$this->get_id()]];
$minScore = $myItemToCheck->getPrerequisiteMinScore();
if (empty($minScore)) {
// Try with mastery_score
$masteryScoreAsMin = $myItemToCheck->get_mastery_score();
if (!empty($masteryScoreAsMin)) {
$minScore = $masteryScoreAsMin;
}
}
if (isset($minScore) && isset($minScore)) {
// Taking min/max prerequisites values see BT#5776
if ($quiz['exe_result'] >= $minScore &&
$quiz['exe_result'] <= $maxScore
) {
$returnstatus = true;
} else {
$explanation = sprintf(
get_lang('YourResultAtXBlocksThisElement'),
$itemToCheck->get_title()
);
$this->prereq_alert = $explanation;
$returnstatus = false;
}
if (!empty($minScore)) {
// Taking min/max prerequisites values see BT#5776
if ($quiz['exe_result'] >= $minScore) {
$this->prereq_alert = '';
$returnstatus = true;
} else {
// Classic way
if ($quiz['exe_result'] >=
$items[$refs_list[$prereqs_string]]->get_mastery_score()
) {
$returnstatus = true;
} else {
$explanation = sprintf(
get_lang('YourResultAtXBlocksThisElement'),
$itemToCheck->get_title()
);
$this->prereq_alert = $explanation;
$returnstatus = false;
}
$explanation = sprintf(
get_lang('YourResultAtXBlocksThisElement'),
$itemToCheck->get_title()
);
$this->prereq_alert = $explanation;
$returnstatus = false;
}
} else {
$this->prereq_alert = get_lang('LearnpathPrereqNotCompleted');
$returnstatus = false;
// Classic way
if ($quiz['exe_result'] >=
$items[$refs_list[$prereqs_string]]->get_mastery_score()
) {
$this->prereq_alert = '';
$returnstatus = true;
} else {
$explanation = sprintf(
get_lang('YourResultAtXBlocksThisElement'),
$itemToCheck->get_title()
);
$this->prereq_alert = $explanation;
$returnstatus = false;
}
}
} else {
$this->prereq_alert = get_lang('LearnpathPrereqNotCompleted');
$returnstatus = false;
}
} else {
// 3. For multiple attempts we check that there are minimum 1 item completed
@@ -2440,7 +2444,6 @@ class learnpathItem
/** @var learnpathItem $myItemToCheck */
$myItemToCheck = $items[$refs_list[$this->get_id()]];
$minScore = $myItemToCheck->getPrerequisiteMinScore();
$maxScore = $myItemToCheck->getPrerequisiteMaxScore();
if (empty($minScore)) {
// Try with mastery_score
@@ -2449,11 +2452,9 @@ class learnpathItem
$minScore = $masteryScoreAsMin;
}
}
if (isset($minScore) && isset($minScore)) {
if (!empty($minScore)) {
// Taking min/max prerequisites values see BT#5776
if ($quiz['exe_result'] >= $minScore &&
$quiz['exe_result'] <= $maxScore
) {
if ($quiz['exe_result'] >= $minScore) {
$returnstatus = true;
break;
} else {
@@ -3629,7 +3630,7 @@ class learnpathItem
public function isLpItemsCompleted()
{
$lp = new Learnpath(api_get_course_id(), $this->lp_id, api_get_user_id());
$lp = new learnpath(api_get_course_id(), $this->lp_id, api_get_user_id());
$count = $lp->getTotalItemsCountWithoutDirs([TOOL_LP_FINAL_ITEM]);
$excludeFailedStatus = !(true === api_get_configuration_value('lp_prerequisit_on_quiz_unblock_if_max_attempt_reached'));
$completed = $lp->get_complete_items_count($excludeFailedStatus, [TOOL_LP_FINAL_ITEM]);
@@ -3640,7 +3641,7 @@ class learnpathItem
public function getLpFinalItem()
{
$lp = new Learnpath(api_get_course_id(), $this->lp_id, api_get_user_id());
$lp = new learnpath(api_get_course_id(), $this->lp_id, api_get_user_id());
return $lp->getFinalItem();
}
+1 -168
View File
@@ -15,174 +15,7 @@ require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script();
/**
* Get one item's details.
*
* @param int LP ID
* @param int user ID
* @param int View ID
* @param int Current item ID
* @param int New item ID
*
* @return string
*/
function initialize_item($lp_id, $user_id, $view_id, $next_item)
{
$debug = 0;
$return = '';
if ($debug) {
error_log('In initialize_item('.$lp_id.','.$user_id.','.$view_id.','.$next_item.')');
}
/*$item_id may be one of:
* -'next'
* -'previous'
* -'first'
* -'last'
* - a real item ID
*/
$mylp = learnpath::getLpFromSession(api_get_course_id(), $lp_id, $user_id);
$mylp->set_current_item($next_item);
if ($debug) {
error_log('In initialize_item() - new item is '.$next_item);
}
$mylp->start_current_item(true);
if (is_object($mylp->items[$next_item])) {
if ($debug) {
error_log('In initialize_item - recovering existing item object '.$next_item, 0);
}
$mylpi = $mylp->items[$next_item];
} else {
if ($debug) {
error_log('In initialize_item - generating new item object '.$next_item, 0);
}
$mylpi = new learnpathItem($next_item, $user_id);
}
if ($mylpi) {
$mylpi->set_lp_view($view_id);
}
/*
* now get what's needed by the SCORM API:
* -score
* -max
* -min
* -lesson_status
* -session_time
* -suspend_data
*/
$myscore = $mylpi->get_score();
$mymax = $mylpi->get_max();
if ('' === $mymax) {
$mymax = "''";
}
$mymin = $mylpi->get_min();
$mylesson_status = $mylpi->get_status();
$mytotal_time = $mylpi->get_scorm_time('js', null, true);
$mymastery_score = $mylpi->get_mastery_score();
$mymax_time_allowed = $mylpi->get_max_time_allowed();
$mylaunch_data = $mylpi->get_launch_data();
$mysession_time = $mylpi->get_total_time();
$mysuspend_data = $mylpi->get_suspend_data();
$mylesson_location = $mylpi->get_lesson_location();
$myic = $mylpi->get_interactions_count();
$myistring = '';
for ($i = 0; $i < $myic; $i++) {
$myistring .= ",[".$i.",'','','','','','','']";
}
if (!empty($myistring)) {
$myistring = substr($myistring, 1);
}
// Obtention des donnees d'objectifs
$mycoursedb = Database::get_course_table(TABLE_LP_IV_OBJECTIVE);
$course_id = api_get_course_int_id();
$mylp_iv_id = $mylpi->db_item_view_id;
$phpobjectives = [];
if (!empty($mylp_iv_id)) {
$sql = "SELECT objective_id, status, score_raw, score_max, score_min
FROM $mycoursedb
WHERE lp_iv_id = $mylp_iv_id AND c_id = $course_id
ORDER BY id ASC;";
$res = Database::query($sql);
while ($row = Database::fetch_row($res)) {
$phpobjectives[] = $row;
}
}
$myobjectives = json_encode($phpobjectives);
$return .=
"olms.score=".$myscore.";".
"olms.max=".$mymax.";".
"olms.min=".$mymin.";".
"olms.lesson_status='".$mylesson_status."';".
"olms.lesson_location='".$mylesson_location."';".
"olms.session_time='".$mysession_time."';".
"olms.suspend_data='".$mysuspend_data."';".
"olms.total_time = '".$mytotal_time."';".
"olms.mastery_score = '".$mymastery_score."';".
"olms.max_time_allowed = '".$mymax_time_allowed."';".
"olms.launch_data = '".$mylaunch_data."';".
"olms.interactions = new Array(".$myistring.");".
//"olms.item_objectives = new Array();" .
"olms.item_objectives = ".$myobjectives.";".
"olms.G_lastError = 0;".
"olms.G_LastErrorMessage = 'No error';".
"olms.finishSignalReceived = 0;";
/*
* and re-initialise the rest (proper to the LMS)
* -lms_lp_id
* -lms_item_id
* -lms_old_item_id
* -lms_new_item_id
* -lms_initialized
* -lms_progress_bar_mode
* -lms_view_id
* -lms_user_id
*/
$mynext = $mylp->get_next_item_id();
$myprevious = $mylp->get_previous_item_id();
$myitemtype = $mylpi->get_type();
$mylesson_mode = $mylpi->get_lesson_mode();
$mycredit = $mylpi->get_credit();
$mylaunch_data = $mylpi->get_launch_data();
$myinteractions_count = $mylpi->get_interactions_count();
$mycore_exit = $mylpi->get_core_exit();
$return .=
"olms.lms_lp_id=".$lp_id.";".
"olms.lms_item_id=".$next_item.";".
"olms.lms_old_item_id=0;".
"olms.lms_initialized=0;".
"olms.lms_view_id=".$view_id.";".
"olms.lms_user_id=".$user_id.";".
"olms.next_item=".$next_item.";".// This one is very important to replace possible literal strings.
"olms.lms_next_item=".$mynext.";".
"olms.lms_previous_item=".$myprevious.";".
"olms.lms_item_type = '".$myitemtype."';".
"olms.lms_item_credit = '".$mycredit."';".
"olms.lms_item_lesson_mode = '".$mylesson_mode."';".
"olms.lms_item_launch_data = '".$mylaunch_data."';".
"olms.lms_item_interactions_count = '".$myinteractions_count."';".
"olms.lms_item_objectives_count = '".$myinteractions_count."';".
"olms.lms_item_core_exit = '".$mycore_exit."';".
"olms.asset_timer = 0;";
$mylp->set_error_msg('');
$mylp->prerequisites_match(); // Check the prerequisites are all complete.
if ($debug) {
error_log('Prereq_match() returned '.htmlentities($mylp->error), 0);
error_log("return = $return ");
error_log("mylp->lp_view_session_id: ".$mylp->lp_view_session_id);
}
if (isset($mylp->lti_launch_id)) {
$ltiLaunchId = $mylp->lti_launch_id;
$return .= "sendLtiLaunch('$ltiLaunchId', '$lp_id');";
}
return $return;
}
require_once __DIR__.'/lp_initialize_item.inc.php';
echo initialize_item(
$_POST['lid'],
+4 -1
View File
@@ -611,9 +611,12 @@ if (isset($_REQUEST['interact'])) {
}
}
// Always use the authenticated user's ID to prevent IDOR — never trust
// the uid from the request, as any enrolled user could overwrite another
// user's Learning Path progress by changing this parameter.
echo save_item(
(!empty($_REQUEST['lid']) ? $_REQUEST['lid'] : null),
(!empty($_REQUEST['uid']) ? $_REQUEST['uid'] : null),
api_get_user_id(),
(!empty($_REQUEST['vid']) ? $_REQUEST['vid'] : null),
(!empty($_REQUEST['iid']) ? $_REQUEST['iid'] : null),
(!empty($_REQUEST['s']) ? $_REQUEST['s'] : null),
+67 -9
View File
@@ -395,10 +395,11 @@ if (!$lp_found || (!empty($_REQUEST['lp_id']) && $_SESSION['oLP']->get_id() != $
}
break;
case 3:
/*
$oLP = new aicc(api_get_course_id(), $lpIid, api_get_user_id());
if ($oLP !== false) {
$lp_found = true;
}
}*/
break;
default:
$oLP = new learnpath(api_get_course_id(), $lpIid, api_get_user_id());
@@ -600,19 +601,19 @@ switch ($action) {
require 'lp_add_item.php';
} else {
Session::write('post_time', $_POST['post_time']);
$directoryParentId = isset($_POST['directory_parent_id']) ? $_POST['directory_parent_id'] : 0;
$directoryParentId = $_POST['directory_parent_id'] ?? 0;
$courseInfo = api_get_course_info();
if (empty($directoryParentId)) {
$_SESSION['oLP']->generate_lp_folder($courseInfo);
}
$parent = isset($_POST['parent']) ? $_POST['parent'] : '';
$previous = isset($_POST['previous']) ? $_POST['previous'] : '';
$type = isset($_POST['type']) ? $_POST['type'] : '';
$path = isset($_POST['path']) ? $_POST['path'] : '';
$description = isset($_POST['description']) ? $_POST['description'] : '';
$prerequisites = isset($_POST['prerequisites']) ? $_POST['prerequisites'] : '';
$maxTimeAllowed = isset($_POST['maxTimeAllowed']) ? $_POST['maxTimeAllowed'] : '';
$parent = $_POST['parent'] ?? '';
$previous = $_POST['previous'] ?? '';
$type = $_POST['type'] ?? '';
$path = $_POST['path'] ?? '';
$description = $_POST['description'] ?? '';
$prerequisites = $_POST['prerequisites'] ?? '';
$maxTimeAllowed = $_POST['maxTimeAllowed'] ?? '';
if ($_POST['type'] == TOOL_DOCUMENT) {
@@ -1238,6 +1239,63 @@ switch ($action) {
require 'lp_list.php';
}
break;
case 'reorder_categories':
if (!api_is_allowed_to_edit(null, true)) {
api_not_allowed(true);
}
$courseId = api_get_course_int_id();
$tableCategory = Database::get_course_table(TABLE_LP_CATEGORY);
$catOrder = isset($_POST['order']) ? (array) $_POST['order'] : [];
$pos = 1;
foreach ($catOrder as $catIid) {
$catIid = (int) $catIid;
if ($catIid <= 0) {
continue;
}
$sql = "UPDATE $tableCategory
SET position = $pos
WHERE c_id = $courseId
AND iid = $catIid";
Database::query($sql);
$pos++;
}
header('Content-Type: application/json');
echo json_encode(['ok' => true]);
exit;
case 'reorder_lps':
if (!api_is_allowed_to_edit(null, true)) {
api_not_allowed(true);
}
$courseId = api_get_course_int_id();
$sessionId = api_get_session_id();
$tableLp = Database::get_course_table(TABLE_LP_MAIN);
$lists = isset($_POST['lists']) ? (array) $_POST['lists'] : [];
foreach ($lists as $categoryIdStr => $lpIds) {
$categoryId = (int) $categoryIdStr;
$pos = 1;
foreach ((array) $lpIds as $lpId) {
$lpId = (int) $lpId;
if ($lpId <= 0) {
continue;
}
$sql = "UPDATE $tableLp
SET category_id = ".($categoryId ?: 0).", display_order = $pos
WHERE c_id = $courseId AND id = $lpId";
Database::query($sql);
$pos++;
}
}
header('Content-Type: application/json');
echo json_encode(['ok' => true]);
exit;
case 'edit':
if (!$is_allowed_to_edit) {
api_not_allowed(true);
+1 -1
View File
@@ -68,7 +68,7 @@ $badgeLink = '';
$finalItemTemplate = '';
// Check prerequisites and total completion of the learning path
$lp = new Learnpath($courseCode, $lpId, $userId);
$lp = new learnpath($courseCode, $lpId, $userId);
$count = $lp->getTotalItemsCountWithoutDirs();
$excludeFailedStatus = !(true === api_get_configuration_value('lp_prerequisit_on_quiz_unblock_if_max_attempt_reached'));
$completed = $lp->get_complete_items_count($excludeFailedStatus);
+177
View File
@@ -0,0 +1,177 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Shared helper for SCORM item initialization.
* Called both by lp_ajax_initialize.php (AJAX path for item switches) and
* scorm_api.php (inline PHP path for the initial item on page load).
*/
/**
* Get one item's details and return the JavaScript string that sets all
* olms.* variables for that item.
*
* @param int $lp_id
* @param int $user_id
* @param int $view_id
* @param int $next_item Current item ID
*
* @return string JavaScript code to be output/eval'd in the browser
*/
function initialize_item($lp_id, $user_id, $view_id, $next_item)
{
$debug = 0;
$return = '';
if ($debug) {
error_log('In initialize_item('.$lp_id.','.$user_id.','.$view_id.','.$next_item.')');
}
/*$item_id may be one of:
* -'next'
* -'previous'
* -'first'
* -'last'
* - a real item ID
*/
$mylp = learnpath::getLpFromSession(api_get_course_id(), $lp_id, $user_id);
$mylp->set_current_item($next_item);
if ($debug) {
error_log('In initialize_item() - new item is '.$next_item);
}
$mylp->start_current_item(true);
if (is_object($mylp->items[$next_item])) {
if ($debug) {
error_log('In initialize_item - recovering existing item object '.$next_item, 0);
}
$mylpi = $mylp->items[$next_item];
} else {
if ($debug) {
error_log('In initialize_item - generating new item object '.$next_item, 0);
}
$mylpi = new learnpathItem($next_item, $user_id);
}
if ($mylpi) {
$mylpi->set_lp_view($view_id);
}
/*
* now get what's needed by the SCORM API:
* -score
* -max
* -min
* -lesson_status
* -session_time
* -suspend_data
*/
$myscore = $mylpi->get_score();
$mymax = $mylpi->get_max();
if ('' === $mymax) {
$mymax = "''";
}
$mymin = $mylpi->get_min();
$mylesson_status = $mylpi->get_status();
$mytotal_time = $mylpi->get_scorm_time('js', null, true);
$mymastery_score = $mylpi->get_mastery_score();
$mymax_time_allowed = $mylpi->get_max_time_allowed();
$mylaunch_data = $mylpi->get_launch_data();
$mysession_time = $mylpi->get_total_time();
$mysuspend_data = $mylpi->get_suspend_data();
$mylesson_location = $mylpi->get_lesson_location();
$myic = $mylpi->get_interactions_count();
$myistring = '';
for ($i = 0; $i < $myic; $i++) {
$myistring .= ",[".$i.",'','','','','','','']";
}
if (!empty($myistring)) {
$myistring = substr($myistring, 1);
}
// Obtention des donnees d'objectifs
$mycoursedb = Database::get_course_table(TABLE_LP_IV_OBJECTIVE);
$course_id = api_get_course_int_id();
$mylp_iv_id = $mylpi->db_item_view_id;
$phpobjectives = [];
if (!empty($mylp_iv_id)) {
$sql = "SELECT objective_id, status, score_raw, score_max, score_min
FROM $mycoursedb
WHERE lp_iv_id = $mylp_iv_id AND c_id = $course_id
ORDER BY id ASC;";
$res = Database::query($sql);
while ($row = Database::fetch_row($res)) {
$phpobjectives[] = $row;
}
}
$myobjectives = json_encode($phpobjectives);
$return .=
"olms.score=".$myscore.";".
"olms.max=".$mymax.";".
"olms.min=".$mymin.";".
"olms.lesson_status='".$mylesson_status."';".
"olms.lesson_location='".$mylesson_location."';".
"olms.session_time='".$mysession_time."';".
"olms.suspend_data='".$mysuspend_data."';".
"olms.total_time = '".$mytotal_time."';".
"olms.mastery_score = '".$mymastery_score."';".
"olms.max_time_allowed = '".$mymax_time_allowed."';".
"olms.launch_data = '".$mylaunch_data."';".
"olms.interactions = new Array(".$myistring.");".
//"olms.item_objectives = new Array();" .
"olms.item_objectives = ".$myobjectives.";".
"olms.G_lastError = 0;".
"olms.G_LastErrorMessage = 'No error';".
"olms.finishSignalReceived = 0;";
/*
* and re-initialise the rest (proper to the LMS)
* -lms_lp_id
* -lms_item_id
* -lms_old_item_id
* -lms_new_item_id
* -lms_initialized
* -lms_progress_bar_mode
* -lms_view_id
* -lms_user_id
*/
$mynext = $mylp->get_next_item_id();
$myprevious = $mylp->get_previous_item_id();
$myitemtype = $mylpi->get_type();
$mylesson_mode = $mylpi->get_lesson_mode();
$mycredit = $mylpi->get_credit();
$mylaunch_data = $mylpi->get_launch_data();
$myinteractions_count = $mylpi->get_interactions_count();
$mycore_exit = $mylpi->get_core_exit();
$return .=
"olms.lms_lp_id=".$lp_id.";".
"olms.lms_item_id=".$next_item.";".
"olms.lms_old_item_id=0;".
"olms.lms_initialized=0;".
"olms.lms_view_id=".$view_id.";".
"olms.lms_user_id=".$user_id.";".
"olms.next_item=".$next_item.";".// This one is very important to replace possible literal strings.
"olms.lms_next_item=".$mynext.";".
"olms.lms_previous_item=".$myprevious.";".
"olms.lms_item_type = '".$myitemtype."';".
"olms.lms_item_credit = '".$mycredit."';".
"olms.lms_item_lesson_mode = '".$mylesson_mode."';".
"olms.lms_item_launch_data = '".$mylaunch_data."';".
"olms.lms_item_interactions_count = '".$myinteractions_count."';".
"olms.lms_item_objectives_count = '".$myinteractions_count."';".
"olms.lms_item_core_exit = '".$mycore_exit."';".
"olms.asset_timer = 0;";
$mylp->set_error_msg('');
$mylp->prerequisites_match(); // Check the prerequisites are all complete.
if ($debug) {
error_log('Prereq_match() returned '.htmlentities($mylp->error), 0);
error_log("return = $return ");
error_log("mylp->lp_view_session_id: ".$mylp->lp_view_session_id);
}
if (isset($mylp->lti_launch_id)) {
$ltiLaunchId = $mylp->lti_launch_id;
$return .= "sendLtiLaunch('$ltiLaunchId', '$lp_id');";
}
return $return;
}
+2
View File
@@ -942,6 +942,7 @@ foreach ($categories as $item) {
);
$listData[] = [
'lp_id' => $id,
'learnpath_icon' => $icon_learnpath,
'url_start' => $url_start_lp,
'title' => $my_title,
@@ -1049,6 +1050,7 @@ $template->assign('lp_is_shown', $lpIsShown);
$template->assign('filtered_category', $filteredCategoryId);
$template->assign('allow_min_time', $allowMinTime);
$template->assign('allow_dates_for_student', $allowDatesForStudent);
$template->assign('sec_token', $token);
$templateName = $template->get_template('learnpath/list.tpl');
$content = $template->fetch($templateName);
+6 -1
View File
@@ -370,7 +370,12 @@ if ($allowUserGroups) {
$items[] = $formUserGroup->toHtml();
}
$menu = $oLP->build_action_menu(true, false, true, false);
$noEdition = false;
if (!isset($sessionId) || $sessionId !== 0) {
$noEdition = true;
}
$menu = $oLP->build_action_menu(true, false, true, false, '', [], $noEdition);
$tpl = new Template();
$tabs = Display::tabs($headers, $items);
+116 -10
View File
@@ -12,6 +12,84 @@ use Chamilo\CourseBundle\Component\CourseCopy\CourseRestorer;
*/
require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script();
/**
* Imports a Chamilo CourseBackup zip that contains LP data into the current course.
* Returns true only if something was actually restored (basic DB verification).
*/
function chamilo_lp_import_from_zip(string $zipPath, int $sessionId): bool
{
$cid = (int) api_get_course_int_id();
// Basic input validation
if (!is_file($zipPath) || !is_readable($zipPath)) {
return false;
}
// Minimal snapshot to detect real changes
$snapshot = static function () use ($cid): array {
$lp = Database::fetch_array(Database::query(
"SELECT COUNT(*) AS cnt, COALESCE(MAX(iid), 0) AS max_iid
FROM c_lp WHERE c_id = ".$cid
));
$lpItem = Database::fetch_array(Database::query(
"SELECT COUNT(*) AS cnt, COALESCE(MAX(iid), 0) AS max_iid
FROM c_lp_item WHERE c_id = ".$cid
));
return [
'lp_cnt' => (int) ($lp['cnt'] ?? 0),
'lp_max' => (int) ($lp['max_iid'] ?? 0),
'lpi_cnt' => (int) ($lpItem['cnt'] ?? 0),
'lpi_max' => (int) ($lpItem['max_iid'] ?? 0),
];
};
$before = $snapshot();
// Store inside course_backups/
$backupFile = CourseArchiver::importUploadedFile($zipPath);
if ($backupFile === false || $backupFile === '') {
return false;
}
// Guard: ensure file is really there before readCourse()
$backupAbs = CourseArchiver::getBackupDir().$backupFile;
if (!is_file($backupAbs) || !is_readable($backupAbs)) {
return false;
}
// true => delete backup zip after extracting
$course = CourseArchiver::readCourse($backupFile, true);
if (!is_object($course)) {
return false;
}
$restorer = new CourseRestorer($course);
$restorer->set_file_option(FILE_OVERWRITE);
// Restore only what LP import needs (avoid side effects from full course restore)
$allowedTools = ['documents', 'learnpath_category', 'learnpaths', 'scorm_documents', 'assets'];
if (isset($restorer->tools_to_restore) && is_array($restorer->tools_to_restore)) {
$restorer->tools_to_restore = array_values(array_intersect($restorer->tools_to_restore, $allowedTools));
}
// Destination MUST be course code (api_get_course_id)
$destination = (string) api_get_course_id();
if ($destination === '') {
return false;
}
$res = $restorer->restore($destination, (int) $sessionId);
if ($res === false) {
return false;
}
$after = $snapshot();
return $after !== $before;
}
$course_dir = api_get_course_path().'/scorm';
$course_sys_dir = api_get_path(SYS_COURSE_PATH).$course_dir;
if (empty($_POST['current_dir'])) {
@@ -68,14 +146,17 @@ if (isset($_POST) && $is_error) {
switch ($type) {
case 'chamilo':
$filename = CourseArchiver::importUploadedFile($_FILES['user_file']['tmp_name']);
if ($filename) {
$course = CourseArchiver::readCourse($filename, false);
$courseRestorer = new CourseRestorer($course);
// FILE_SKIP, FILE_RENAME or FILE_OVERWRITE
$courseRestorer->set_file_option(FILE_OVERWRITE);
$courseRestorer->restore('', api_get_session_id());
Display::addFlash(Display::return_message(get_lang('UplUploadSucceeded')));
if (empty($_configuration['allow_lp_chamilo_export'])) {
Display::addFlash(Display::return_message(get_lang('ScormUnknownPackageFormat'), 'warning'));
break;
}
$ok = chamilo_lp_import_from_zip($_FILES['user_file']['tmp_name'], api_get_session_id());
if ($ok) {
Display::addFlash(Display::return_message(get_lang('UplUploadSucceeded'), 'confirmation'));
} else {
Display::addFlash(Display::return_message(get_lang('UploadError'), 'error'));
}
break;
case 'scorm':
@@ -98,6 +179,8 @@ if (isset($_POST) && $is_error) {
$oScorm->set_jslib('scorm_api.php');
break;
case 'aicc':
Display::addFlash(Display::return_message(get_lang('ScormUnknownPackageFormat'), 'warning'));
/*
$oAICC = new aicc();
$config_dir = $oAICC->import_package($_FILES['user_file']);
if (!empty($config_dir)) {
@@ -108,7 +191,8 @@ if (isset($_POST) && $is_error) {
$oAICC->set_proximity($proximity);
$oAICC->set_maker($maker);
$oAICC->set_jslib('aicc_api.php');
break;
*/
return false;
case 'oogie':
require_once 'openoffice_presentation.class.php';
$take_slide_name = empty($_POST['take_slide_name']) ? false : true;
@@ -168,6 +252,23 @@ if (isset($_POST) && $is_error) {
$type = learnpath::getPackageType($s, basename($s));
switch ($type) {
case 'chamilo':
if (empty($_configuration['allow_lp_chamilo_export'])) {
$is_error = true;
Display::addFlash(Display::return_message(get_lang('UnknownPackageFormat'), 'error'));
break;
}
$ok = chamilo_lp_import_from_zip($s, api_get_session_id());
@unlink($s);
if ($ok) {
Display::addFlash(Display::return_message(get_lang('UplUploadSucceeded'), 'confirmation'));
} else {
$is_error = true;
Display::addFlash(Display::return_message(get_lang('UploadError'), 'error'));
}
break;
case 'scorm':
$oScorm = new scorm();
$manifest = $oScorm->import_local_package($s, $current_dir);
@@ -192,6 +293,10 @@ if (isset($_POST) && $is_error) {
$oScorm->set_jslib('scorm_api.php');
break;
case 'aicc':
if (is_file($s)) {
unlink($s);
}
/*
$oAICC = new aicc();
$config_dir = $oAICC->import_local_package($s, $current_dir);
// The file was treated, it can now be cleaned from the temp dir
@@ -212,7 +317,8 @@ if (isset($_POST) && $is_error) {
$oAICC->set_proximity($proximity);
$oAICC->set_maker($maker);
$oAICC->set_jslib('aicc_api.php');
break;
*/
return false;
case '':
default:
// There was an error, clean the file from the temp dir
+2
View File
@@ -334,6 +334,7 @@ if (!isset($src)) {
break;
case 3:
// aicc
/*
$lp->stop_previous_item(); // save status manually if asset
$htmlHeadXtra[] = '<script src="'.$lp->get_js_lib().'" type="text/javascript" language="javascript"></script>';
$preReqCheck = $lp->prerequisites_match($lp_item_id);
@@ -347,6 +348,7 @@ if (!isset($src)) {
} else {
$src = 'blank.php';
}
*/
break;
case 4:
break;
+120 -16
View File
@@ -49,6 +49,18 @@ $autocomplete_when_80pct = 0;
$user = api_get_user_info();
$userId = api_get_user_id();
// Pre-load initialization data for the initial item via PHP so that
// LMSInitialize() can skip its synchronous AJAX call on first load.
require_once __DIR__.'/lp_initialize_item.inc.php';
$initialItemId = $oItem->get_id();
$initialItemViewId = $oLP->get_view(null, $userId) ?: 1;
$initialItemScript = initialize_item(
$oLP->get_id(),
$userId,
$initialItemViewId,
$initialItemId
);
$extraParams = '';
if (isset($oLP->lti_launch_id)) {
$extraParams .= '&lti_launch_id='.Security::remove_XSS($oLP->lti_launch_id);
@@ -222,7 +234,13 @@ olms.userfname = '<?php echo addslashes(trim($user['firstname'])); ?>';
olms.userlname = '<?php echo addslashes(trim($user['lastname'])); ?>';
olms.execute_stats = false;
var courseUrl = '?cidReq='+olms.lms_course_code+'&id_session='+olms.lms_session_id+'<?php echo $extraParams; ?>';
// Initialization data for the initial item, pre-loaded by PHP.
// LMSInitialize() uses this to skip its synchronous AJAX call on first load.
// For subsequent items (after navigation), LMSInitialize() still uses AJAX.
var olmsPreloadedItemId = <?php echo (int) $initialItemId; ?>;
<?php echo $initialItemScript; ?>
var courseUrl ='?cidReq='+olms.lms_course_code+'&id_session='+olms.lms_session_id+'<?php echo $extraParams; ?>';
var statsUrl = 'lp_controller.php' + courseUrl + '&action=stats';
/**
@@ -299,24 +317,50 @@ function LMSInitialize() {
//reinit the list of modified variables
reinit_updatable_vars_list();
// Get LMS values for this item
var params = {
'lid': olms.lms_lp_id,
'uid': olms.lms_user_id,
'vid': olms.lms_view_id,
'iid': olms.lms_item_id
};
if (typeof olmsPreloadedItemId !== 'undefined' && olmsPreloadedItemId == olms.lms_item_id) {
// Initial item: initialization data was pre-loaded by PHP inline.
// No AJAX call needed; all olms.* variables are already set.
logit_scorm('LMSInitialize() using pre-loaded data for item ' + olms.lms_item_id);
olmsPreloadedItemId = undefined; // clear so subsequent items use AJAX
$('video:not(.skip), audio:not(.skip)').mediaelementplayer();
} else {
// Subsequent items (after navigation): use pre-fetched data if
// switch_item() had time to complete the fetch, otherwise fall
// back to synchronous AJAX (should be extremely rare in practice).
var nextItemId = parseInt(olms.lms_item_id, 10);
var cached = olmsInitDataCache[nextItemId];
$.ajax({
type: "POST",
url: "lp_ajax_initialize.php" + courseUrl,
data: params,
dataType: 'script',
async: false,
success:function(data) {
if (cached && cached.resolved) {
logit_scorm('LMSInitialize() using pre-fetched data for item ' + nextItemId);
$.globalEval(cached.script);
delete olmsInitDataCache[nextItemId];
$('video:not(.skip), audio:not(.skip)').mediaelementplayer();
} else {
// Pre-fetch either hasn't resolved yet or failed. This is the
// rare fallback path (sync AJAX, still deprecated but harmless
// as a safety net until pre-fetch covers all cases reliably).
if (cached) {
logit_scorm('LMSInitialize() pre-fetch not ready for item ' + nextItemId + ', using sync AJAX fallback', 1);
delete olmsInitDataCache[nextItemId];
}
var params = {
'lid': olms.lms_lp_id,
'uid': olms.lms_user_id,
'vid': olms.lms_view_id,
'iid': olms.lms_item_id
};
$.ajax({
type: "POST",
url: "lp_ajax_initialize.php" + courseUrl,
data: params,
dataType: 'script',
async: false,
success: function(data) {
$('video:not(.skip), audio:not(.skip)').mediaelementplayer();
}
});
}
});
}
olms.lms_initialized = 1;
olms.switch_finished = 1;
@@ -1496,6 +1540,61 @@ function reinit_updatable_vars_list() {
olms.lesson_status = defaultStatus;
}
/**
* Cache for pre-fetched item initialization data, keyed by numeric item ID.
* Each entry: {resolved: bool, script: string|null, failed: bool}
*/
var olmsInitDataCache = {};
/**
* Start an async fetch of initialization data for the given item ID.
* Called from switch_item() as soon as the target item ID is known, so the
* data is ready (or very nearly so) by the time LMSInitialize() fires after
* the new content loads in the iframe.
* Falls back gracefully: if this fetch fails or hasn't resolved by the time
* LMSInitialize() is called, a synchronous AJAX call is used instead.
*/
function prefetchInitData(itemId) {
itemId = parseInt(itemId, 10);
if (!itemId || olmsInitDataCache[itemId]) {
return; // invalid ID, or already fetching/fetched for this item
}
var entry = {resolved: false, script: null, failed: false};
var body = 'lid=' + encodeURIComponent(olms.lms_lp_id) +
'&uid=' + encodeURIComponent(olms.lms_user_id) +
'&vid=' + encodeURIComponent(olms.lms_view_id) +
'&iid=' + encodeURIComponent(itemId);
if (typeof fetch === 'function') {
fetch('lp_ajax_initialize.php' + courseUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: body
})
.then(function(response) {
if (!response.ok) {
throw new Error('HTTP ' + response.status);
}
return response.text();
})
.then(function(script) {
entry.script = script;
entry.resolved = true;
})
.catch(function(err) {
entry.failed = true;
logit_lms('prefetchInitData() failed for item ' + itemId + ': ' + err, 1);
});
} else {
// fetch() not available (IE11); mark as failed so LMSInitialize()
// uses its synchronous AJAX fallback.
entry.failed = true;
}
olmsInitDataCache[itemId] = entry;
}
/**
* Function that handles the saving of an item and switching from an item to another.
* Once called, this function should be able to do the whole process of
@@ -1702,6 +1801,11 @@ function switch_item(current_item, next_item)
break;
}
// Start fetching init data for the next item now, in parallel with the
// iframe loading its content. By the time the SCO calls LMSInitialize(),
// the data will almost certainly already be available.
prefetchInitData(next_item);
var mysrc = '<?php echo api_get_path(WEB_CODE_PATH); ?>lp/lp_controller.php?action=content&lp_id=' + olms.lms_lp_id +
'&item_id=' + next_item + '&cidReq=' + olms.lms_course_code + '&id_session=' + olms.lms_session_id;
var cont_f = $("#content_id");