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
+1 -1
View File
@@ -95,7 +95,7 @@ if ($usergroup->allowTeachers()) {
$onlyThisSessionList = array_column($sessionList, 'id');
}
}
$session_list = SessionManager::get_sessions_list([], ['name'], null, null, 0, $onlyThisSessionList);
$session_list = SessionManager::get_sessions_list([], ['name'], null, null, 0, $onlyThisSessionList, true);
$elements_not_in = $elements_in = [];
if (!empty($session_list)) {
+7 -5
View File
@@ -543,11 +543,13 @@ if (api_is_platform_admin()) {
$blockPlatform['items'] = $items;
} elseif (api_is_session_admin()) {
$items = [];
$items[] = [
'class' => 'item-stats',
'url' => 'statistics/index.php',
'label' => get_lang('Statistics'),
];
if (api_get_configuration_value('session_admin_access_global_statistics')) {
$items[] = [
'class' => 'item-stats',
'url' => 'statistics/index.php',
'label' => get_lang('Statistics'),
];
}
if (api_get_configuration_value('session_admin_access_system_announcement')) {
$items[] = [
'class' => 'item-global-announcement',
+10 -3
View File
@@ -960,8 +960,16 @@ switch ($report) {
case 'users_active':
$content = '';
if ($validated) {
$startDate = $values['daterange_start'];
$endDate = $values['daterange_end'];
// Validate date inputs strictly. Security::remove_XSS() (used for
// the display value above) does not protect against SQL injection.
$rawStartDate = isset($values['daterange_start']) ? $values['daterange_start'] : '';
$rawEndDate = isset($values['daterange_end']) ? $values['daterange_end'] : '';
$parsedStart = !empty($rawStartDate) ? DateTime::createFromFormat('Y-m-d', $rawStartDate) : false;
$parsedEnd = !empty($rawEndDate) ? DateTime::createFromFormat('Y-m-d', $rawEndDate) : false;
$startDate = (false !== $parsedStart && $parsedStart->format('Y-m-d') === $rawStartDate)
? Database::escape_string($rawStartDate) : '';
$endDate = (false !== $parsedEnd && $parsedEnd->format('Y-m-d') === $rawEndDate)
? Database::escape_string($rawEndDate) : '';
$graph = '<div class="row">';
$graph .= '<div class="col-md-4"><canvas id="canvas1" style="margin-bottom: 20px"></canvas></div>';
@@ -986,7 +994,6 @@ switch ($report) {
$conditions = [];
$extraConditions = '';
if (!empty($startDate) && !empty($endDate)) {
// $extraConditions is already cleaned inside the function getUserListExtraConditions
$extraConditions .= " AND registration_date BETWEEN '$startDate' AND '$endDate' ";
}
+26 -23
View File
@@ -278,30 +278,33 @@ $form->addGroup($group, 'mail', get_lang('SendMailToNewUser'));
$hideNeverExpiresOpt = api_get_configuration_value('user_hide_never_expire_option');
$lblExpiration = '';
$defaultExpiration = 0;
if ($hideNeverExpiresOpt) {
$lblExpiration = get_lang('ExpirationDate');
$defaultExpiration = 1;
$group = [];
$group[] = $form->createElement('radio', 'radio_expiration_date', get_lang('ExpirationDate'), get_lang('Enabled'), 1);
$group[] = $form->createElement(
'DateTimePicker',
'expiration_date',
null
);
} else {
$form->addElement('radio', 'radio_expiration_date', get_lang('ExpirationDate'), get_lang('NeverExpires'), 0);
$group = [];
$group[] = $form->createElement('radio', 'radio_expiration_date', null, get_lang('Enabled'), 1);
$group[] = $form->createElement(
'DateTimePicker',
'expiration_date',
null,
[
'onchange' => 'javascript: enable_expiration_date();',
]
);
$hideExpirationDate = api_get_configuration_value('user_hide_expiration_date_for_session_admin');
if (!$hideExpirationDate || api_is_platform_admin()) {
if ($hideNeverExpiresOpt && !api_is_platform_admin()) {
$lblExpiration = get_lang('ExpirationDate');
$defaultExpiration = 1;
$group = [];
$group[] = $form->createElement('radio', 'radio_expiration_date', get_lang('ExpirationDate'), get_lang('Enabled'), 1);
$group[] = $form->createElement(
'DateTimePicker',
'expiration_date',
null
);
} else {
$form->addElement('radio', 'radio_expiration_date', get_lang('ExpirationDate'), get_lang('NeverExpires'), 0);
$group = [];
$group[] = $form->createElement('radio', 'radio_expiration_date', null, get_lang('Enabled'), 1);
$group[] = $form->createElement(
'DateTimePicker',
'expiration_date',
null,
[
'onchange' => 'javascript: enable_expiration_date();',
]
);
}
$form->addGroup($group, 'max_member_group', $lblExpiration, null, false);
}
$form->addGroup($group, 'max_member_group', $lblExpiration, null, false);
// Active account or inactive account
$form->addElement('radio', 'active', get_lang('ActiveAccount'), get_lang('Active'), 1);
+27 -24
View File
@@ -12,7 +12,7 @@ $this_section = SECTION_PLATFORM_ADMIN;
api_protect_admin_script(true);
$user_id = isset($_GET['user_id']) ? (int) $_GET['user_id'] : (int) $_POST['user_id'];
api_protect_super_admin($user_id, null, true);
api_protect_super_admin($user_id, null, true !== api_get_configuration_value('disallow_session_admin_edit_users'));
$is_platform_admin = api_is_platform_admin() ? 1 : 0;
$userInfo = api_get_user_info($user_id);
$userEntity = api_get_user_entity($user_id);
@@ -320,31 +320,34 @@ $form->addElement('label', get_lang('RegistrationDate'), $date);
$defaultExpiration = 0;
if (!$user_data['platform_admin']) {
$hideNeverExpiresOpt = api_get_configuration_value('user_hide_never_expire_option');
$hideExpirationDate = api_get_configuration_value('user_hide_expiration_date_for_session_admin');
$lblExpiration = '';
if ($hideNeverExpiresOpt) {
$lblExpiration = get_lang('ExpirationDate');
$defaultExpiration = 1;
$group = [];
$group[] = $form->createElement('radio', 'radio_expiration_date', get_lang('ExpirationDate'), get_lang('Enabled'), 1);
$group[] = $form->createElement(
'DateTimePicker',
'expiration_date',
null
);
} else {
$form->addElement('radio', 'radio_expiration_date', get_lang('ExpirationDate'), get_lang('NeverExpires'), 0);
$group = [];
$group[] = $form->createElement('radio', 'radio_expiration_date', null, get_lang('Enabled'), 1);
$group[] = $form->createElement(
'DateTimePicker',
'expiration_date',
null,
[
'onchange' => 'javascript: enable_expiration_date();',
]
);
if (!$hideExpirationDate || api_is_platform_admin()) {
if ($hideNeverExpiresOpt && !api_is_platform_admin()) {
$lblExpiration = get_lang('ExpirationDate');
$defaultExpiration = 1;
$group = [];
$group[] = $form->createElement('radio', 'radio_expiration_date', get_lang('ExpirationDate'), get_lang('Enabled'), 1);
$group[] = $form->createElement(
'DateTimePicker',
'expiration_date',
null
);
} else {
$form->addElement('radio', 'radio_expiration_date', get_lang('ExpirationDate'), get_lang('NeverExpires'), 0);
$group = [];
$group[] = $form->createElement('radio', 'radio_expiration_date', null, get_lang('Enabled'), 1);
$group[] = $form->createElement(
'DateTimePicker',
'expiration_date',
null,
[
'onchange' => 'javascript: enable_expiration_date();',
]
);
}
$form->addGroup($group, 'max_member_group', $lblExpiration, null, false);
}
$form->addGroup($group, 'max_member_group', $lblExpiration, null, false);
// Active account or inactive account
$form->addElement('radio', 'active', get_lang('ActiveAccount'), get_lang('Active'), 1);
+220 -94
View File
@@ -255,14 +255,12 @@ function complete_missing_data(array $user): array
/**
* Save the imported data.
*
* @uses \global variable $inserted_in_course, which returns the list of
* courses the user was inserted in
* @uses global $inserted_in_course, which returns the list of courses the user was inserted in
*
* @return array The $users array, with 'message' and (for reused users) 'id' set
*/
function save_data(
array $users,
bool $sendMail = false,
?string $targetFolder = null
): array {
function save_data(array $users, bool $sendMail = false, ?string $targetFolder = null): array
{
global $inserted_in_course, $extra_fields;
// Not all scripts declare the $inserted_in_course array (although they should).
@@ -281,107 +279,216 @@ function save_data(
$optionsByField = [];
// which extrafield variable are we validating on?
$uniqueField = api_get_configuration_value('extra_field_to_validate_on_user_registration');
foreach ($users as &$user) {
if ($user['has_error']) {
$userError[] = $user;
continue;
}
$user = complete_missing_data($user);
$user['Status'] = api_status_key($user['Status']);
$redirection = $user['Redirection'] ?? '';
$returnMessage = '';
$user_id = null;
$user_id = UserManager::create_user(
$user['FirstName'],
$user['LastName'],
$user['Status'],
$user['Email'],
$user['UserName'],
$user['Password'],
$user['OfficialCode'],
$user['language'],
$user['PhoneNumber'],
'',
$user['AuthSource'],
$user['ExpiryDate'],
1,
0,
null,
null,
$sendMail,
false,
'',
false,
null,
null,
null,
$redirection
);
// 1) If the CSV row has that unique extrafield, try to look up an existing user
if (!empty($uniqueField) && !empty($user[$uniqueField])) {
// pass true to getUserId mode
$existing = UserManager::isExtraFieldValueUniquePerUrl($user[$uniqueField], true);
if ($existing !== null) {
// existing user found → reuse
$user_id = $existing;
$returnMessage = Display::return_message(
sprintf(
get_lang('ExistingUserWithSameExtraFieldValue'),
$uniqueField,
$existing
),
'info'
);
$userInfo = api_get_user_info($user_id);
$firstName = $user['FirstName'] ?? $userInfo['firstname'];
$lastName = $user['LastName'] ?? $userInfo['lastname'];
$userName = $userInfo['username'];
if (!empty($user['UserName'])) {
$userName = $user['UserName'];
}
$changePassMethod = 0;
$password = null;
$authSource = $userInfo['auth_source'];
if ($user_id) {
$returnMessage = Display::return_message(get_lang('UserAdded'), 'success');
if (isset($user['Password'])) {
$changePassMethod = 2;
$password = $user['Password'];
}
if (isset($user['Courses']) && is_array($user['Courses'])) {
foreach ($user['Courses'] as $course) {
if (CourseManager::course_exists($course)) {
$result = CourseManager::subscribeUser($user_id, $course, $user['Status']);
if ($result) {
$course_info = api_get_course_info($course);
$inserted_in_course[$course] = $course_info['title'];
}
if (isset($user['AuthSource']) && $user['AuthSource'] != $authSource) {
$authSource = $user['AuthSource'];
$changePassMethod = 3;
}
$email = $user['Email'] ?? $userInfo['email'];
$status = $user['Status'] ?? $userInfo['status'];
$officialCode = $user['OfficialCode'] ?? $userInfo['official_code'];
$phone = $user['PhoneNumber'] ?? $userInfo['phone'];
$pictureUrl = $user['PictureUri'] ?? $userInfo['picture_uri'];
$expirationDate = $user['ExpiryDate'] ?? $userInfo['expiration_date'];
// Fix wrong date in DB for old users (sometimes would be expiration_date = '9999-12-31 ********') where it should be null
if (substr($expirationDate, 0, 4) === '9999') {
$expirationDate = null;
}
$active = $userInfo['active'];
if (isset($user['Active'])) {
$user['Active'] = (int) $user['Active'];
if (-1 === $user['Active']) {
$user['Active'] = 0;
}
$active = $user['Active'];
}
$creatorId = $userInfo['creator_id'];
$hrDeptId = $userInfo['hr_dept_id'];
$language = $user['Language'] ?? $userInfo['language'];
UserManager::update_user(
$user_id,
$firstName,
$lastName,
$userName,
$password,
$authSource,
$email,
$status,
$officialCode,
$phone,
$pictureUrl,
$expirationDate,
$active,
$creatorId,
$hrDeptId,
$extra,
$language,
'',
false,
$changePassMethod
);
}
if (isset($user['Sessions']) && is_array($user['Sessions'])) {
foreach ($user['Sessions'] as $sessionId) {
$sessionInfo = api_get_session_info($sessionId);
if (!empty($sessionInfo)) {
SessionManager::subscribeUsersToSession(
$sessionId,
[$user_id],
SESSION_VISIBLE_READ_ONLY,
false
);
}
}
}
if (!empty($user['ClassId'])) {
$classId = explode('|', trim($user['ClassId']));
foreach ($classId as $id) {
$usergroup->subscribe_users_to_usergroup($id, [$user_id], false);
}
}
// We are sure that the extra field exists.
foreach ($extra_fields as $extras) {
if (!isset($user[$extras[1]])) {
continue;
}
$key = $extras[1];
$value = $user[$key];
if (!array_key_exists($key, $optionsByField)) {
$optionsByField[$key] = $efo->getOptionsByFieldVariable($key);
}
/** @var ExtraFieldOptions $option */
foreach ($optionsByField[$key] as $option) {
if ($option->getDisplayText() === $value) {
$value = $option->getValue();
}
}
UserManager::update_extra_field_value($user_id, $key, $value);
}
$userSaved[] = $user;
} else {
$returnMessage = Display::return_message(get_lang('Error'), 'warning');
$userWarning[] = $user;
}
// 2) If not found, go through normal creation
if ($user_id === null) {
// fill in missing fields, generate password, etc.
$user = complete_missing_data($user);
$user['Status'] = api_status_key($user['Status']);
$redirection = $user['Redirection'] ?? '';
$user_id = UserManager::create_user(
$user['FirstName'],
$user['LastName'],
$user['Status'],
$user['Email'],
$user['UserName'],
$user['Password'],
$user['OfficialCode'],
$user['language'],
$user['PhoneNumber'],
'',
$user['AuthSource'],
$user['ExpiryDate'],
1,
0,
null,
null,
$sendMail,
false,
'',
false,
null,
null,
null,
$redirection
);
if ($user_id) {
$returnMessage = Display::return_message(get_lang('UserAdded'), 'success');
} else {
$returnMessage = Display::return_message(get_lang('Error'), 'error');
$userWarning[] = $user;
$user['message'] = $returnMessage;
continue;
}
}
// 3) At this point $user_id is either reused or newly created.
// Enroll in courses:
if (isset($user['Courses']) && is_array($user['Courses'])) {
foreach ($user['Courses'] as $course) {
if (CourseManager::course_exists($course)) {
$result = CourseManager::subscribeUser($user_id, $course, $user['Status']);
if ($result) {
$info = api_get_course_info($course);
$inserted_in_course[$course] = $info['title'];
}
}
}
}
// 4) Enroll in sessions:
if (isset($user['Sessions']) && is_array($user['Sessions'])) {
foreach ($user['Sessions'] as $sessionId) {
$sessionInfo = api_get_session_info($sessionId);
if (!empty($sessionInfo)) {
SessionManager::subscribeUsersToSession(
$sessionId,
[$user_id],
SESSION_VISIBLE_READ_ONLY,
false
);
}
}
}
// 5) Subscribe to usergroups:
if (!empty($user['ClassId'])) {
$classIds = explode('|', trim($user['ClassId']));
foreach ($classIds as $id) {
$usergroup->subscribe_users_to_usergroup($id, [$user_id], false);
}
}
// 6) Update extrafield values (for newly created or even reused users):
foreach ($extra_fields as $extras) {
$fieldVar = $extras[1];
$matchedKey = null;
foreach ($user as $colName => $colVal) {
if (strtolower($colName) === strtolower($fieldVar)) {
$matchedKey = $colName;
break;
}
}
if ($matchedKey === null) {
continue;
}
$value = $user[$matchedKey];
if (!array_key_exists($matchedKey, $optionsByField)) {
$optionsByField[$matchedKey] = $efo->getOptionsByFieldVariable($matchedKey);
}
/** @var ExtraFieldOptions $option */
foreach ($optionsByField[$matchedKey] as $option) {
if ($option->getDisplayText() === $value) {
$value = $option->getValue();
break;
}
}
UserManager::update_extra_field_value($user_id, $matchedKey, $value);
}
// 7) Record success
$user['id'] = $user_id;
$user['message'] = $returnMessage;
$userSaved[] = $user;
}
// Save with success, error and warning users
@@ -708,6 +815,25 @@ if (isset($_POST['formSent']) && $_POST['formSent'] && $_FILES['import_file']['s
Session::erase('user_import_data_'.$userId);
$users = Import::csvToArray($_FILES['import_file']['tmp_name']);
$uniqueField = api_get_configuration_value('extra_field_to_validate_on_user_registration');
if (!empty($uniqueField) && !empty($users)) {
$firstRow = reset($users);
$csvHeader = array_keys($firstRow);
$csvHeaderLower = array_map('trim', array_map('strtolower', $csvHeader));
if (!in_array($uniqueField, $csvHeaderLower, true)) {
Display::addFlash(
Display::return_message(
sprintf('The column "%s" is required in the CSV for this platform', $uniqueField),
'error'
)
);
header('Location: '.api_get_self());
exit;
}
}
$users = parse_csv_data(
$users,
$cleanFileName,
+17 -3
View File
@@ -155,6 +155,7 @@ function trimVariables()
'keyword_username',
'keyword_email',
'keyword_officialcode',
'keyword_phone',
];
foreach ($filterVariables as $variable) {
@@ -235,6 +236,7 @@ function prepare_user_sql_query($getCount)
'keyword_username',
'keyword_email',
'keyword_officialcode',
'keyword_phone',
'keyword_status',
'keyword_active',
'keyword_inactive',
@@ -264,7 +266,8 @@ function prepare_user_sql_query($getCount)
concat(u.lastname,' ',u.firstname) LIKE '$keywordFiltered' OR
u.username LIKE '$keywordFiltered' OR
u.official_code LIKE '$keywordFiltered' OR
u.email LIKE '$keywordFiltered'
u.email LIKE '$keywordFiltered' OR
u.phone LIKE '$keywordFiltered'
)
";
} elseif (isset($keywordListValues) && !empty($keywordListValues)) {
@@ -308,6 +311,9 @@ function prepare_user_sql_query($getCount)
if (!empty($keywordListValues['keyword_officialcode'])) {
$sql .= " AND u.official_code LIKE '".Database::escape_string("%".$keywordListValues['keyword_officialcode']."%")."' ";
}
if (!empty($keywordListValues['keyword_phone'])) {
$sql .= " AND u.phone LIKE '".Database::escape_string("%".$keywordListValues['keyword_phone']."%")."' ";
}
$sql .= " $keyword_admin $keyword_extra_value ";
@@ -643,8 +649,12 @@ function modify_filter($user_id, $url_params, $row)
if (api_is_platform_admin(true)) {
$editProfileUrl = Display::getProfileEditionLink($user_id, true);
if (!$user_is_anonymous &&
api_global_admin_can_edit_admin($user_id, null, true)
if (!$user_is_anonymous
&& api_global_admin_can_edit_admin(
$user_id,
null,
true !== api_get_configuration_value('disallow_session_admin_edit_users')
)
) {
$result .= '<a href="'.$editProfileUrl.'">'.
Display::return_icon(
@@ -985,6 +995,9 @@ if (isset($_GET['keyword'])) {
$parameters['keyword_email'] = Security::remove_XSS($_GET['keyword_email']);
$parameters['keyword_officialcode'] = Security::remove_XSS($_GET['keyword_officialcode']);
$parameters['keyword_status'] = Security::remove_XSS($_GET['keyword_status']);
if (isset($_GET['keyword_phone'])) {
$parameters['keyword_phone'] = Security::remove_XSS($_GET['keyword_phone']);
}
if (isset($_GET['keyword_active'])) {
$parameters['keyword_active'] = Security::remove_XSS($_GET['keyword_active']);
}
@@ -1014,6 +1027,7 @@ $form->addText('keyword_lastname', get_lang('LastName'), false);
$form->addText('keyword_username', get_lang('LoginName'), false);
$form->addText('keyword_email', get_lang('Email'), false);
$form->addText('keyword_officialcode', get_lang('OfficialCode'), false);
$form->addText('keyword_phone', get_lang('Phone'), false);
$classId = isset($_REQUEST['class_id']) && !empty($_REQUEST['class_id']) ? (int) $_REQUEST['class_id'] : 0;
$options = [];
+9 -1
View File
@@ -37,6 +37,8 @@ if (isset($_REQUEST['load_ajax'])) {
}
$user_id = (int) $_REQUEST['user_id'];
$new_course_list = SessionManager::get_course_list_by_session_id($new_session_id);
$coursesInOldSession = SessionManager::get_course_list_by_session_id($origin_session_id);
$countCoursesInOldSession = count($coursesInOldSession);
$course_founded = false;
foreach ($new_course_list as $course_item) {
@@ -55,13 +57,14 @@ if (isset($_REQUEST['load_ajax'])) {
// Check if the same course exist in the session destination
if ($course_founded) {
$result = SessionManager::get_users_by_session($new_session_id);
$subscribedToNew = false;
if (empty($result) || !in_array($user_id, array_keys($result))) {
if ($debug) {
echo 'User added to the session';
}
// Registering user to the new session
if ($update_database) {
SessionManager::subscribeUsersToSession(
$subscribedToNew = SessionManager::subscribeUsersToSession(
$new_session_id,
[$user_id],
false,
@@ -80,6 +83,11 @@ if (isset($_REQUEST['load_ajax'])) {
$update_database,
$debug
);
// If there is only one course in the old session, remove the user from the old session (otherwise leads to confusion)
if ($update_database && $subscribedToNew && $countCoursesInOldSession === 1) {
SessionManager::unsubscribe_user_from_session($origin_session_id, $user_id);
echo get_lang('UserUnsubscribedFromOldSessionAsThereWasOnlyOneCourse');
}
} else {
echo get_lang('CourseDoesNotExistInThisSession');
}
+9 -1
View File
@@ -649,7 +649,12 @@ class AttendanceController
// Get data table
$data_table = [];
$head_table = ['#', get_lang('Name')];
$addOfficialCode = api_get_configuration_value('attendance_add_official_code');
if ($addOfficialCode) {
$head_table = ['#', get_lang('OfficialCode'), get_lang('Name')];
} else {
$head_table = ['#', get_lang('Name')];
}
foreach ($data_array['attendant_calendar'] as $class_day) {
$labelDuration = !empty($class_day['duration']) ? get_lang('Duration').' : '.$class_day['duration'] : '';
$head_table[] =
@@ -667,6 +672,9 @@ class AttendanceController
$cols = 1;
$result = [];
$result['count'] = $count;
if ($addOfficialCode) {
$result['official_code'] = $user['official_code'];
}
$result['full_name'] = api_get_person_name($user['firstname'], $user['lastname']);
foreach ($data_array['attendant_calendar'] as $class_day) {
if ($class_day['done_attendance'] == 1) {
+11 -1
View File
@@ -210,6 +210,11 @@ if (api_is_allowed_to_edit(null, true) ||
});
</script>
<?php $addOfficialCode = api_get_configuration_value('attendance_add_official_code');
$headerOfficialCode = '';
if ($addOfficialCode) {
$headerOfficialCode = '<th width="100px">'.get_lang('OfficialCode').'</th>';
} ?>
<form method="post" action="index.php?action=attendance_sheet_add&<?php echo api_get_cidreq().$param_filter; ?>&attendance_id=<?php echo $attendance_id; ?>" >
<div class="attendance-sheet-content" style="width:100%;background-color:#E1E1E1;margin-top:20px;">
<div class="divTableWithFloatingHeader attendance-users-table" style="width:45%;float:left;margin:0px;padding:0px;">
@@ -218,6 +223,7 @@ if (api_is_allowed_to_edit(null, true) ||
<tr class="tableFloatingHeader" style="position: absolute; top: 0px; left: 0px; visibility: hidden; margin:0px;padding:0px" >
<th width="10px"><?php echo '#'; ?></th>
<th width="10px"><?php echo get_lang('Photo'); ?></th>
<?php echo $headerOfficialCode; ?>
<th width="100px"><?php echo get_lang('LastName'); ?></th>
<th width="100px"><?php echo get_lang('FirstName'); ?></th>
<th width="100px"><?php echo get_lang('AttendancesFaults'); ?></th>
@@ -225,6 +231,7 @@ if (api_is_allowed_to_edit(null, true) ||
<tr class="tableFloatingHeaderOriginal" >
<th width="10px"><?php echo '#'; ?></th>
<th width="10px"><?php echo get_lang('Photo'); ?></th>
<?php echo $headerOfficialCode; ?>
<th width="150px"><?php echo get_lang('LastName'); ?></th>
<th width="140px"><?php echo get_lang('FirstName'); ?></th>
<th width="100px"><?php echo get_lang('AttendancesFaults'); ?></th>
@@ -248,6 +255,9 @@ if (api_is_allowed_to_edit(null, true) ||
<tr class="<?php echo $class; ?>">
<td><center><?php echo $i; ?></center></td>
<td><?php echo $data['photo']; ?></td>
<?php if ($addOfficialCode) {
echo '<td>'.$data['official_code'].'</td>';
} ?>
<td><span title="<?php echo $username; ?>"><?php echo $data['lastname']; ?></span></td>
<td><?php echo $data['firstname']; ?></td>
<td>
@@ -286,7 +296,7 @@ if (api_is_allowed_to_edit(null, true) ||
if ($allowSignature) {
$iconFullScreen = Display::url(
Display::return_icon('view_fullscreen.png', get_lang('SeeForTablet'), [], ICON_SIZE_SMALL),
api_get_self().'?'.api_get_cidreq().'&action=attendance_sheet_list&func=fullscreen&attendance_id='.$attendance_id.'&calendar_id='.$calendar['id'] . (!empty($groupId) ? '&group_id=' . $groupId : '')
api_get_self().'?'.api_get_cidreq().'&action=attendance_sheet_list&func=fullscreen&attendance_id='.$attendance_id.'&calendar_id='.$calendar['id'].(!empty($groupId) ? '&group_id='.$groupId : '')
);
$isBlocked = 0;
$iconBlockName = 'eyes.png';
+10 -5
View File
@@ -89,20 +89,25 @@ if ($form->validate()) {
$user = Login::get_user_accounts_by_username($values['user']);
if (!$user) {
$messageText = get_lang('NoUserAccountWithThisEmailAddress');
// Always return the same neutral response regardless of whether the
// username/email exists. Revealing "no account with this email" allows
// user enumeration by observing the differing redirect destination or
// message. Using the same message and redirect as the success path
// prevents that information leak.
$messageText = get_lang('AnEmailToResetYourPasswordHasBeenSent');
if (CustomPages::enabled() && CustomPages::exists(CustomPages::LOST_PASSWORD)) {
if (CustomPages::enabled() && CustomPages::exists(CustomPages::INDEX_UNLOGGED)) {
CustomPages::display(
CustomPages::LOST_PASSWORD,
CustomPages::INDEX_UNLOGGED,
['info' => $messageText]
);
exit;
}
Display::addFlash(
Display::return_message($messageText, 'error', false)
Display::return_message($messageText, 'info', false)
);
header('Location: '.api_get_self());
header('Location: '.api_get_path(WEB_PATH));
exit;
}
+1 -1
View File
@@ -165,7 +165,7 @@ function _openid_parse_message($message) {
*/
function _openid_nonce() {
// YYYY-MM-DDThh:mm:ssTZD UTC, plus some optional extra unique chars
return gmstrftime('%Y-%m-%dT%H:%M:%S%Z') .
return gmdate('Y-m-d\TH:i:sT') .
chr(mt_rand(0, 25) + 65) .
chr(mt_rand(0, 25) + 65) .
chr(mt_rand(0, 25) + 65) .
+43 -36
View File
@@ -2,12 +2,16 @@
/* For licensing terms, see /license.txt */
use Symfony\Component\HttpFoundation\Request as HttpRequest;
$cidReset = true; // Flag forcing the 'current course' reset
require_once __DIR__.'/../inc/global.inc.php';
api_block_anonymous_users();
$httpRequest = HttpRequest::createFromGlobals();
$auth = new Auth();
$user_course_categories = CourseManager::get_user_course_categories(api_get_user_id());
$courses_in_category = $auth->getCoursesInCategory(false);
@@ -21,8 +25,11 @@ $authorizedActions = [
'set_collapsable',
'unsubscribe',
];
if (in_array(trim($_REQUEST['action']), $authorizedActions)) {
$action = trim($_REQUEST['action']);
$action = $httpRequest->query->get('action', $httpRequest->request->get('action', ''));
if (!in_array($action, $authorizedActions)) {
$action = '';
}
$currentUrl = api_get_self();
@@ -33,9 +40,9 @@ $interbreadcrumb[] = [
];
// We are moving the course of the user to a different user defined course category (=Sort My Courses).
if (isset($_POST['submit_change_course_category'])) {
$course2EditCategory = Security::remove_XSS($_POST['course_2_edit_category']);
$courseCategories = Security::remove_XSS($_POST['course_categories']);
if ($httpRequest->request->has('submit_change_course_category')) {
$course2EditCategory = Security::remove_XSS($httpRequest->request->get('course_2_edit_category'));
$courseCategories = Security::remove_XSS($httpRequest->request->get('course_categories'));
$result = $auth->updateCourseCategory($course2EditCategory, $courseCategories);
if ($result) {
Display::addFlash(
@@ -47,16 +54,20 @@ if (isset($_POST['submit_change_course_category'])) {
}
// We edit course category
if (isset($_POST['submit_edit_course_category']) &&
isset($_POST['title_course_category'])
if ($httpRequest->request->has('submit_edit_course_category')
&& $httpRequest->request->has('title_course_category')
&& Security::check_token('post')
) {
$titleCourseCategory = Security::remove_XSS($_POST['title_course_category']);
$categoryId = Security::remove_XSS($_POST['category_id']);
$result = $auth->store_edit_course_category($titleCourseCategory, $categoryId);
if ($result) {
Display::addFlash(
Display::return_message(get_lang('CourseCategoryEditStored'))
);
$titleCourseCategory = Security::remove_XSS($httpRequest->request->get('title_course_category'));
$categoryId = Security::remove_XSS($httpRequest->request->get('category_id'));
$categoryInfo = $auth->getUserCourseCategory($categoryId);
if ($categoryInfo) {
$result = $auth->store_edit_course_category($titleCourseCategory, $categoryId);
if ($result) {
Display::addFlash(
Display::return_message(get_lang('CourseCategoryEditStored'))
);
}
}
header('Location: '.api_get_self());
@@ -64,11 +75,10 @@ if (isset($_POST['submit_edit_course_category']) &&
}
// We are creating a new user defined course category (= Create Course Category).
if (isset($_POST['create_course_category']) &&
isset($_POST['title_course_category']) &&
strlen(trim($_POST['title_course_category'])) > 0
if ($httpRequest->request->has('create_course_category')
&& $titleCourseCategory = $httpRequest->request->get('title_course_category')
) {
$titleCourseCategory = Security::remove_XSS($_POST['title_course_category']);
$titleCourseCategory = Security::remove_XSS($titleCourseCategory);
$result = $auth->store_course_category($titleCourseCategory);
if ($result) {
Display::addFlash(
@@ -87,10 +97,10 @@ if (isset($_POST['create_course_category']) &&
}
// We are moving a course or category of the user up/down the list (=Sort My Courses).
if (isset($_GET['move'])) {
$getCourse = isset($_GET['course']) ? Security::remove_XSS($_GET['course']) : '';
$getMove = Security::remove_XSS($_GET['move']);
$getCategory = isset($_GET['category']) ? Security::remove_XSS($_GET['category']) : '';
if ($getMove = $httpRequest->query->get('move')) {
$getCourse = Security::remove_XSS($httpRequest->query->get('course'));
$getMove = Security::remove_XSS($getMove);
$getCategory = Security::remove_XSS($httpRequest->query->get('category'));
if (!empty($getCourse)) {
$result = $auth->move_course($getMove, $getCourse, $getCategory);
if ($result) {
@@ -113,7 +123,7 @@ if (isset($_GET['move'])) {
switch ($action) {
case 'edit_category':
$categoryId = isset($_GET['category_id']) ? (int) $_GET['category_id'] : 0;
$categoryId = $httpRequest->query->getInt('category_id');
$categoryInfo = $auth->getUserCourseCategory($categoryId);
if ($categoryInfo) {
$categoryName = $categoryInfo['title'];
@@ -124,15 +134,15 @@ switch ($action) {
);
$form->addText('title_course_category', get_lang('Name'));
$form->addHidden('category_id', $categoryId);
$form->addHidden('sec_token', Security::get_token());
$form->addButtonSave(get_lang('Edit'), 'submit_edit_course_category');
$form->setDefaults(['title_course_category' => $categoryName]);
$form->display();
}
exit;
break;
case 'edit_course_category':
$edit_course = (int) $_GET['course_id'];
$defaultCategoryId = isset($_GET['category_id']) ? (int) $_GET['category_id'] : 0;
$edit_course = $httpRequest->query->getInt('course_id');
$defaultCategoryId = $httpRequest->query->getInt('category_id');
$courseInfo = api_get_course_info_by_id($edit_course);
if (empty($courseInfo)) {
@@ -167,12 +177,10 @@ switch ($action) {
$form->addButtonSave(get_lang('Save'), 'submit_change_course_category');
$form->display();
exit;
break;
case 'deletecoursecategory':
// we are deleting a course category
if (isset($_GET['id'])) {
if ($getId = $httpRequest->query->getInt('id')) {
if (Security::check_token('get')) {
$getId = Security::remove_XSS($_GET['id']);
$result = $auth->delete_course_category($getId);
if ($result) {
Display::addFlash(
@@ -183,7 +191,6 @@ switch ($action) {
}
header('Location: '.api_get_self());
exit;
break;
case 'createcoursecategory':
$form = new FormValidator(
'create_course_category',
@@ -194,16 +201,15 @@ switch ($action) {
$form->addButtonSave(get_lang('AddCategory'), 'create_course_category');
$form->display();
exit;
break;
case 'set_collapsable':
if (!api_get_configuration_value('allow_user_course_category_collapsable')) {
api_not_allowed(true);
}
$userId = api_get_user_id();
$categoryId = isset($_REQUEST['categoryid']) ? (int) $_REQUEST['categoryid'] : 0;
$option = isset($_REQUEST['option']) ? (int) $_REQUEST['option'] : 0;
$redirect = isset($_REQUEST['redirect']) ? Security::remove_XSS($_REQUEST['redirect']) : 0;
$categoryId = $httpRequest->query->getInt('categoryid', $httpRequest->request->getInt('categoryid'));
$option = $httpRequest->query->get('option', $httpRequest->request->getInt('option'));
$redirect = $httpRequest->query->get('redirect', $httpRequest->request->get('redirect', ''));
if (empty($userId) || empty($categoryId)) {
api_not_allowed(true);
@@ -225,7 +231,6 @@ switch ($action) {
$url = api_get_self();
header('Location: '.$url);
exit;
break;
}
function generateUnsubscribeForm(string $courseCode, string $secToken): string
@@ -433,7 +438,9 @@ if (!empty($courses_without_category)) {
);
}
echo '';
if (isset($_GET['edit']) && $course['code'] == $_GET['edit']) {
if ($httpRequest->query->has('edit')
&& $httpRequest->query->get('edit') === $course['code']
) {
echo Display::return_icon('edit_na.png', get_lang('Edit'), '', 22);
} else {
echo Display::url(
+31 -8
View File
@@ -3,6 +3,7 @@
use Chamilo\CoreBundle\Entity\Skill;
use Skill as SkillManager;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
/**
* Page for assign skills to a user.
@@ -11,7 +12,12 @@ use Skill as SkillManager;
*/
require_once __DIR__.'/../inc/global.inc.php';
$userId = isset($_REQUEST['user']) ? (int) $_REQUEST['user'] : 0;
$httpRequest = HttpRequest::createFromGlobals();
$userId = $httpRequest->query->getInt(
'user',
$httpRequest->request->getInt('user')
);
if (empty($userId)) {
api_not_allowed(true);
@@ -53,11 +59,25 @@ if (empty($skillLevels)) {
$skillsOptions[$skill['data']['id']] = $skill['data']['name'];
}
}
$skillIdFromGet = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : 0;
$currentValue = isset($_REQUEST['current_value']) ? (int) $_REQUEST['current_value'] : 0;
$currentLevel = isset($_REQUEST['current']) ? (int) str_replace('sub_skill_id_', '', $_REQUEST['current']) : 0;
$skillIdFromGet = $httpRequest->query->getInt(
'id',
$httpRequest->request->getInt('id')
);
$currentValue = $httpRequest->query->getInt(
'current_value',
$httpRequest->request->getInt('current_value')
);
$currentLevel = $httpRequest->query->get(
'current',
$httpRequest->request->get('current', '')
);
$currentLevel = (int) str_replace('sub_skill_id_', '', $currentLevel);
$subSkillList = isset($_REQUEST['sub_skill_list']) ? explode(',', $_REQUEST['sub_skill_list']) : [];
$subSkillList = $httpRequest->query->get(
'sub_skill_list',
$httpRequest->request->get('sub_skill_list', '')
);
$subSkillList = explode(',', $subSkillList);
$subSkillList = array_unique($subSkillList);
if (!empty($subSkillList)) {
@@ -94,7 +114,10 @@ if (!empty($currentLevel)) {
}
}
$skillId = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : key($skillsOptions);
$skillId = $httpRequest->query->getInt(
'id',
$httpRequest->request->getInt('id', key($skillsOptions))
);
$skill = $skillRepo->find($skillId);
$profile = false;
if ($skill) {
@@ -234,8 +257,7 @@ if ($showLevels) {
//$form->addRule('acquired_level', get_lang('ThisFieldIsRequired'), 'required');
}
$form->addTextarea('argumentation', get_lang('Argumentation'), ['rows' => 6]);
$form->addRule('argumentation', get_lang('ThisFieldIsRequired'), 'required');
$form->addTextarea('argumentation', get_lang('Argumentation'), ['rows' => 6], true);
$form->addRule(
'argumentation',
sprintf(get_lang('ThisTextShouldBeAtLeastXCharsLong'), 10),
@@ -243,6 +265,7 @@ $form->addRule(
10
);
$form->applyFilter('argumentation', 'trim');
$form->applyFilter('argumentation', 'html_filter');
$form->addButtonSave(get_lang('Save'));
$form->setDefaults($formDefaultValues);
+8 -2
View File
@@ -4,6 +4,7 @@
use Chamilo\CoreBundle\Entity\SkillRelUser;
use Chamilo\CoreBundle\Entity\SkillRelUserComment;
use SkillRelUser as SkillRelUserManager;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
/**
* Show information about the issued badge.
@@ -13,7 +14,12 @@ use SkillRelUser as SkillRelUserManager;
*/
require_once __DIR__.'/../inc/global.inc.php';
$issue = isset($_REQUEST['issue']) ? (int) $_REQUEST['issue'] : 0;
$httpRequest = HttpRequest::createFromGlobals();
$issue = $httpRequest->query->getInt(
'issue',
$httpRequest->request->getInt('issue')
);
if (empty($issue)) {
api_not_allowed(true);
@@ -103,7 +109,7 @@ $skillIssueInfo = [
'acquired_level' => $currentSkillLevel,
'argumentation_author_id' => $skillIssue->getArgumentationAuthorId(),
'argumentation_author_name' => $author['complete_name'],
'argumentation' => $skillIssue->getArgumentation(),
'argumentation' => Security::remove_XSS($skillIssue->getArgumentation()),
'source_name' => $skillIssue->getSourceName(),
'user_id' => $skillIssue->getUser()->getId(),
'user_complete_name' => UserManager::formatUserFullName($skillIssue->getUser()),
+6 -3
View File
@@ -4,6 +4,7 @@
use Chamilo\CoreBundle\Entity\SkillRelUser;
use Chamilo\CoreBundle\Entity\SkillRelUserComment;
use SkillRelUser as SkillRelUserManager;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
/**
* Show information about all issued badges with same skill by user.
@@ -12,8 +13,10 @@ use SkillRelUser as SkillRelUserManager;
*/
require_once __DIR__.'/../inc/global.inc.php';
$userId = isset($_GET['user']) ? (int) $_GET['user'] : 0;
$skillId = isset($_GET['skill']) ? (int) $_GET['skill'] : 0;
$httpRequest = HttpRequest::createFromGlobals();
$userId = $httpRequest->query->getInt('user');
$skillId = $httpRequest->query->getInt('skill');
if (!$userId || !$skillId) {
api_not_allowed(true);
@@ -90,7 +93,7 @@ foreach ($userSkills as $index => $skillIssue) {
$argumentationAuthor['firstname'],
$argumentationAuthor['lastname']
),
'argumentation' => $skillIssue->getArgumentation(),
'argumentation' => Security::remove_XSS($skillIssue->getArgumentation()),
'source_name' => $skillIssue->getSourceName(),
'user_id' => $skillIssue->getUser()->getId(),
'user_complete_name' => UserManager::formatUserFullName($skillIssue->getUser()),
+6 -3
View File
@@ -18,7 +18,7 @@ if ((!api_is_allowed_in_course() || !api_is_allowed_in_course()) && !api_is_allo
}
$origin = api_get_origin();
$action = isset($_GET['action']) ? $_GET['action'] : '';
$action = $_GET['action'] ?? '';
if (api_is_allowed_to_edit()) {
$nameTools = get_lang('blog_management');
@@ -58,11 +58,14 @@ if (api_is_allowed_to_edit()) {
echo Display::return_message(get_lang('BlogEdited'), 'confirmation');
}
}
if (isset($_GET['action']) && $_GET['action'] == 'visibility') {
$isValidToken = Security::check_token('get', null, 'blog');
if ($action == 'visibility' && $isValidToken) {
Blog::changeBlogVisibility(intval($_GET['blog_id']));
echo Display::return_message(get_lang('VisibilityChanged'), 'confirmation');
}
if (isset($_GET['action']) && $_GET['action'] == 'delete') {
if ($action == 'delete' && $isValidToken) {
Blog::deleteBlog(intval($_GET['blog_id']));
echo Display::return_message(get_lang('BlogDeleted'), 'confirmation');
}
+1 -1
View File
@@ -55,7 +55,7 @@ if ($action === 'course_select_form' && Security::check_token('post')) {
// Get admin details
$adminId = (int) $_POST['admin_id'];
$adminUsername = filter_var($_POST['admin_username'], FILTER_SANITIZE_STRING);
$adminUsername = strip_tags((string) $_POST['admin_username']);
if (!preg_match('/^[a-zA-Z0-9_]+$/', $adminUsername)) {
echo Display::return_message(get_lang('PleaseEnterValidLogin'), 'error');
exit();
+100 -64
View File
@@ -30,10 +30,7 @@ $domain = 'example.com'; // Manually configured domain for generated emails
// Include Chamilo bootstrap and necessary classes
require_once __DIR__.'/../../main/inc/global.inc.php';
// Include PHPExcel classes (assuming installed via Composer)
require_once __DIR__.'/../../vendor/autoload.php';
require_once __DIR__.'/../../vendor/phpoffice/phpexcel/Classes/PHPExcel.php';
require_once __DIR__.'/../../vendor/phpoffice/phpexcel/Classes/PHPExcel/IOFactory.php';
// Command-line arguments parsing (without getopt)
$proceed = false;
@@ -88,8 +85,8 @@ global $database;
// Load XLSX file
try {
$inputFileType = PHPExcel_IOFactory::identify($xlsxFile);
$reader = PHPExcel_IOFactory::createReader($inputFileType);
$inputFileType = \PhpOffice\PhpSpreadsheet\IOFactory::identify($xlsxFile);
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader($inputFileType);
$phpExcel = $reader->load($xlsxFile);
$worksheet = $phpExcel->getActiveSheet();
$xlsxRows = $worksheet->toArray();
@@ -105,7 +102,7 @@ $xlsxColumnMap = [
'Mail' => 'email',
'Matricule' => 'official_code',
'N° de badge' => 'password',
'Tel mobile' => 'phone',
'tel mobile' => 'phone',
'Actif' => 'active',
];
@@ -128,6 +125,7 @@ $xlsxEmailCounts = [];
$xlsxNameCounts = [];
$duplicateEmails = [];
$duplicateNames = [];
$generatedEmails = []; // username -> generatedEmail
$xlsxUsernames = []; // Store usernames from XLSX
// Output columns for missing field and duplicate files
@@ -146,8 +144,8 @@ function normalizeName($name)
function removeAccents($str)
{
$str = str_replace(
['à', 'á', 'â', 'ã', 'ä', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', 'ù', 'ú', 'û', 'ü', 'ý', 'ÿ', 'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', 'Ù', 'Ú', 'Û', 'Ü', 'Ý'],
['a', 'a', 'a', 'a', 'a', 'c', 'e', 'e', 'e', 'e', 'i', 'i', 'i', 'i', 'n', 'o', 'o', 'o', 'o', 'o', 'u', 'u', 'u', 'u', 'y', 'y', 'A', 'A', 'A', 'A', 'A', 'A', 'C', 'E', 'E', 'E', 'E', 'I', 'I', 'I', 'I', 'N', 'O', 'O', 'O', 'O', 'O', 'U', 'U', 'U', 'U', 'Y'],
['à', 'á', 'â', 'ã', 'ä', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', 'ù', 'ú', 'û', 'ü', 'ý', 'ÿ', 'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', "'", ""],
['a', 'a', 'a', 'a', 'a', 'c', 'e', 'e', 'e', 'e', 'i', 'i', 'i', 'i', 'n', 'o', 'o', 'o', 'o', 'o', 'u', 'u', 'u', 'u', 'y', 'y', 'A', 'A', 'A', 'A', 'A', 'A', 'C', 'E', 'E', 'E', 'E', 'I', 'I', 'I', 'I', 'N', 'O', 'O', 'O', 'O', 'O', 'U', 'U', 'U', 'U', 'Y', '', ''],
$str
);
@@ -177,37 +175,22 @@ function generateProposedLogin($xlsxLastname, $xlsxFirstname, $isActive, &$usedL
$lastFirstnamePart = end($firstnameParts);
$lastPartLetters = strtolower(preg_replace('/[\s-]+/', '', $lastFirstnamePart));
// Increment occurrence count for this login
$usedLogins['counts'][$login] = isset($usedLogins['counts'][$login]) ? $usedLogins['counts'][$login] + 1 : 1;
$occurrence = $usedLogins['counts'][$login];
// Handle duplicates
if (isset($usedLogins['logins'][$login])) {
// Only modify if both current and previous users are active
if ($isActive && $usedLogins['logins'][$login]['active']) {
if ($occurrence == 2) {
// Second occurrence: append next letter from last firstname part
if (strlen($lastPartLetters) > 1) {
$login = $baseLogin.substr($lastPartLetters, 1, 1); // e.g., 'i' from 'Pierre'
} else {
$login = $baseLogin.'1'; // Fallback if no more letters
}
} elseif ($occurrence >= 3) {
// Third+ occurrence: append increasing letters from last firstname part
$extraLetters = min($occurrence - 1, strlen($lastPartLetters) - 1); // e.g., 2 letters for 3rd, 3 for 4th
if ($extraLetters > 0) {
$login = $baseLogin.substr($lastPartLetters, 1, $extraLetters); // e.g., 'ii', 'iii'
} else {
$login = $baseLogin.($occurrence - 1); // Fallback to number
}
// Handle duplicates by incrementally adding letters from the last firstname part if active
if ($isActive) {
$letterCount = 0;
while (isset($usedLogins['logins'][$login]) && $usedLogins['logins'][$login]['active']) {
$letterCount++;
if ($letterCount > strlen($lastPartLetters) - 1) {
break; // No more letters available. Will append a number below
}
$login = $baseLogin.substr($lastPartLetters, 1, $letterCount);
}
}
// Ensure uniqueness by appending a number if still conflicting
$suffix = 1;
$originalLogin = $login;
while (isset($usedLogins['logins'][$login])) {
while (isset($usedLogins['logins'][$login]) && $usedLogins['logins'][$login]['active']) {
$login = $originalLogin.$suffix;
$suffix++;
}
@@ -227,21 +210,21 @@ function createMissingFieldFile($filename, $rows, $columns)
return;
}
$phpExcel = new PHPExcel();
$phpExcel = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$worksheet = $phpExcel->getActiveSheet();
foreach ($columns as $colIndex => $column) {
$worksheet->setCellValueByColumnAndRow($colIndex, 1, $column);
$worksheet->setCellValueByColumnAndRow($colIndex + 1, 1, $column);
}
foreach ($rows as $rowIndex => $rowData) {
foreach ($columns as $colIndex => $column) {
$worksheet->setCellValueByColumnAndRow($colIndex, $rowIndex + 2, $rowData[$column]);
$worksheet->setCellValueByColumnAndRow($colIndex + 1, $rowIndex + 2, $rowData[$column]);
}
}
try {
$writer = PHPExcel_IOFactory::createWriter($phpExcel, 'Excel2007');
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($phpExcel, 'Xlsx');
$writer->save($filename);
echo "Generated $filename with ".count($rows)." rows\n";
} catch (Exception $e) {
@@ -249,6 +232,19 @@ function createMissingFieldFile($filename, $rows, $columns)
}
}
/**
* Generate a tentative e-mail address from firstname and lastname.
*/
function generateMailFromFirstAndLastNames(string $firstname, string $lastname, string $domain): string
{
$emailLastnameParts = preg_split('/[\s-]+/', trim(removeAccents($lastname)), -1, PREG_SPLIT_NO_EMPTY);
$emailLastname = !empty($emailLastnameParts[0]) ? strtolower($emailLastnameParts[0]) : '';
$emailFirstnameParts = preg_split('/[\s-]+/', trim(removeAccents($firstname)), -1, PREG_SPLIT_NO_EMPTY);
$emailFirstname = !empty($emailFirstnameParts[0]) ? strtolower($emailFirstnameParts[0]) : '';
return "$emailLastname.$emailFirstname@$domain";
}
// Detect potential issues in XLSX file
$usedLogins = ['logins' => [], 'counts' => []];
$generatedEmailCounts = [];
@@ -295,27 +291,21 @@ foreach ($xlsxRows as $rowIndex => $xlsxRow) {
];
if ($isActive) {
if (empty($xlsxUserData['email']) && strpos($xlsxUserData['official_code'], '0009') !== false) {
$emailLastnameParts = preg_split('/[\s-]+/', trim(removeAccents($xlsxUserData['lastname'])), -1, PREG_SPLIT_NO_EMPTY);
$emailLastname = !empty($emailLastnameParts[0]) ? strtolower($emailLastnameParts[0]) : '';
$emailFirstnameParts = preg_split('/[\s-]+/', trim(removeAccents($xlsxUserData['firstname'])), -1, PREG_SPLIT_NO_EMPTY);
$emailFirstname = !empty($emailFirstnameParts[0]) ? strtolower($emailFirstnameParts[0]) : '';
$baseEmail = "{$emailLastname}.{$emailFirstname}@{$domain}";
$generatedEmail = $baseEmail;
if (empty($xlsxUserData['email'])) {
$generatedEmail = $baseEmail = generateMailFromFirstAndLastNames($xlsxUserData['firstname'], $xlsxUserData['lastname'], $domain);
$suffix = isset($generatedEmailCounts[$baseEmail]) ? count($generatedEmailCounts[$baseEmail]) + 1 : 1;
if ($suffix > 1) {
$generatedEmail = "{$emailLastname}.{$emailFirstname}{$suffix}@{$domain}";
$generatedEmail = preg_replace('/^([^@]+)@(.+)/', '${1}'.$suffix.'@${2}', $baseEmail);
}
$generatedEmail = strtoupper($generatedEmail);
$generatedEmailCounts[$baseEmail][] = $rowData;
$rowData['Mail'] = $generatedEmail;
$xlsxUserData['email'] = $generatedEmail;
$xlsxUserData['emailSource'] = 'Generated during import';
$emailMissing[] = $rowData;
$xlsxEmailCounts[$generatedEmail][] = $rowData;
} elseif (empty($xlsxUserData['email'])) {
$emailMissing[] = $rowData;
$generatedEmails[$xlsxUserData['official_code']] = [$generatedEmail];
}
if (empty($xlsxUserData['lastname'])) {
@@ -362,7 +352,7 @@ createMissingFieldFile($outputDir.'duplicate_name.xlsx', $duplicateNames, $outpu
// Generate unmatched_db_users.xlsx
$unmatchedUsers = [];
$sql = "SELECT id, username, official_code, email FROM user";
$sql = "SELECT id, username, official_code, email, active FROM user";
$stmt = $database->query($sql);
while ($dbUser = $stmt->fetch()) {
if (!in_array($dbUser['username'], $xlsxUsernames) && !empty($dbUser['username'])) {
@@ -371,10 +361,11 @@ while ($dbUser = $stmt->fetch()) {
'Username' => $dbUser['username'],
'User ID' => $dbUser['id'],
'E-mail' => $dbUser['email'],
'Active' => $dbUser['active'] ? 'Yes' : 'No',
];
}
}
$unmatchedColumns = ['Matricule', 'Username', 'User ID', 'E-mail'];
$unmatchedColumns = ['Matricule', 'Username', 'User ID', 'E-mail', 'Active'];
createMissingFieldFile($outputDir.'unmatched_db_users.xlsx', $unmatchedUsers, $unmatchedColumns);
// Process users: compare with database, log decisions, and update/insert if --proceed
@@ -383,8 +374,10 @@ $userManager = new UserManager();
$usedLogins = ['logins' => [], 'counts' => []]; // Reset usedLogins to avoid false duplicates
$emptyRowCount = 0;
$userActions = []; // Initialize array to store user actions
$userSkippedWhileActive = []; // Initialize array to store special cases
foreach ($xlsxRows as $rowIndex => $rowData) {
// Check for empty row
$emailSource = 'SAP';
$isEmpty = true;
foreach ($rowData as $cell) {
if (!empty(trim($cell))) {
@@ -413,26 +406,36 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
$xlsxUserData['username'] = generateProposedLogin($xlsxUserData['lastname'], $xlsxUserData['firstname'], $isActive, $usedLogins);
$dbUsername = Database::escape_string($xlsxUserData['username']);
if (!empty($xlsxUserData['official_code']) && !empty($generatedEmails[$xlsxUserData['official_code']])) {
$emailSource = 'E-mail generated during import';
$xlsxUserData['email'] = $generatedEmails[$xlsxUserData['official_code']];
} elseif (!empty($rowData['emailSource'])) {
$emailSource = $rowData['emailSource'];
}
// Get current time for row logging
$rowTime = new DateTime();
// Skip users with Matricule starting with 0009
if (strpos($xlsxUserData['official_code'], '0009') === 0) {
echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Skipped - Matricule starts with 0009 (username: $dbUsername)\n";
$userActions[] = [
$logRow = [
'Action Type' => 'skipped',
'User ID' => '',
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxUserData['official_code'],
'Updated Fields' => 'Matricule starts with 0009',
];
$userActions[] = $logRow;
$userSkippedWhileActive[] = $logRow;
continue;
}
// Check for existing user by username
$sql = "SELECT id, firstname, lastname, email, official_code, phone, active
$sql = "SELECT id, firstname, lastname, email, official_code, phone, active, status, picture_uri, expiration_date, language, creator_id
FROM user
WHERE username = '$dbUsername'";
$stmt = $database->query($sql);
@@ -445,14 +448,16 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
// Decision logic
if (empty($dbUser) && empty($xlsxUserData['active'])) {
echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Skipped - 'Actif' is empty and no matching user in database (username: $dbUsername)\n";
$emailSource = 'Not relevant (user ignored)';
$userActions[] = [
'Action Type' => 'skipped',
'User ID' => '',
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Actif is empty and no matching user in database',
'Updated Fields' => '"Active" field is empty and no matching user in database',
];
continue;
}
@@ -463,23 +468,30 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
foreach ($requiredFields as $field) {
if (empty($xlsxUserData[$field])) {
$missingFields[] .= $field;
if ($field == 'email') {
$emailSource = 'EMPTY IN SAP';
}
}
}
if (!empty($missingFields)) {
echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).': Skipped - missing fields: '.implode(', ', $missingFields)." (username: $dbUsername)\n";
$userActions[] = [
$logRow = [
'Action Type' => 'skipped',
'User ID' => '',
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Missing fields: '.implode(', ', $missingFields),
];
$userActions[] = $logRow;
$userSkippedWhileActive[] = $logRow;
continue;
}
// If the user was found/existed in the local database
if ($dbUser) {
// Check for updates
$updates = [];
@@ -515,12 +527,16 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
null, // password not updated
null, // auth_source
$xlsxUserData['email'],
null, // status
$dbUser['status'], // status
$xlsxUserData['official_code'],
$xlsxUserData['phone'],
null, // picture_uri
null, // expiration_date
$xlsxActive
$dbUser['picture_uri'], // picture_uri
$dbUser['expiration_date'], // expiration_date
$xlsxActive,
$dbUser['creator_id'],
0,
null,
$dbUser['language']
);
if ($user) {
// Update extra field 'external_user_id'
@@ -532,32 +548,39 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule,
'Updated Fields' => implode(', ', array_map(function ($update) { return trim(explode(':', $update)[0]); }, $updates)),
];
} else {
echo " Error: Could not update user (username: $dbUsername)\n";
$userActions[] = [
$logRow = [
'Action Type' => 'skipped',
'User ID' => $dbUser['id'],
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Could not update user',
];
$userActions[] = $logRow;
$userSkippedWhileActive[] = $logRow;
}
} catch (Exception $e) {
echo " Error: Failed to update user (username: $dbUsername): {$e->getMessage()}\n";
$userActions[] = [
$logRow = [
'Action Type' => 'skipped',
'User ID' => $dbUser['id'],
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Failed to update user: '.$e->getMessage(),
];
$userActions[] = $logRow;
$userSkippedWhileActive[] = $logRow;
}
} else {
echo " Sim mode: Updated user and external_user_id (username: $dbUsername)\n";
@@ -567,21 +590,25 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule,
'Updated Fields' => implode(', ', array_map(function ($update) { return trim(explode(':', $update)[0]); }, $updates)),
];
}
} else {
echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": No action - no changes needed (username: $dbUsername)\n";
$userActions[] = [
$logRow = [
'Action Type' => 'skipped',
'User ID' => $dbUser['id'],
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule,
'Updated Fields' => 'No changes needed',
];
$userActions[] = $logRow;
$userSkippedWhileActive[] = $logRow;
}
} else {
echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Insert new user - No existing user found (username: $dbUsername)\n";
@@ -602,7 +629,7 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
null,
null,
$xlsxActive,
null,
0,
null // creator_id
);
if ($userId) {
@@ -615,32 +642,39 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule,
'Updated Fields' => '',
];
} else {
echo " Error: Could not create user (username: $dbUsername)\n";
$userActions[] = [
$logRow = [
'Action Type' => 'skipped',
'User ID' => '',
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Could not create user',
];
$userActions[] = $logRow;
$userSkippedWhileActive[] = $logRow;
}
} catch (Exception $e) {
echo " Error: Failed to insert user (username: $dbUsername): {$e->getMessage()}\n";
$userActions[] = [
$logRow = [
'Action Type' => 'skipped',
'User ID' => '',
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Failed to insert user: '.$e->getMessage(),
];
$userActions[] = $logRow;
$userSkippedWhileActive[] = $logRow;
}
} else {
echo " Sim mode: Inserted user and external_user_id (username: $dbUsername)\n";
@@ -650,6 +684,7 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule,
'Updated Fields' => '',
];
@@ -658,8 +693,9 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
}
// Generate user actions XLSX file
$actionColumns = ['Action Type', 'User ID', 'Username', 'Official Code', 'E-mail', 'External User ID', 'Updated Fields'];
$actionColumns = ['Action Type', 'User ID', 'Username', 'Official Code', 'E-mail', 'E-mail source', 'External User ID', 'Updated Fields'];
createMissingFieldFile($outputDir.'user_actions.xlsx', $userActions, $actionColumns);
createMissingFieldFile($outputDir.'skipped_user_actions.xlsx', $userSkippedWhileActive, $actionColumns);
if (!$proceed) {
echo "\nUse --proceed to apply changes to the database.\n";
+3 -9
View File
@@ -215,15 +215,9 @@ class Draggable extends Question
{
$header = parent::return_header($exercise, $counter, $score);
$header .= '<table class="'.$this->question_table_class.'"><tr>';
if ($exercise->showExpectedChoice()) {
$header .= '<th>'.get_lang('YourChoice').'</th>';
if ($exercise->showExpectedChoiceColumn()) {
$header .= '<th>'.get_lang('ExpectedChoice').'</th>';
}
} else {
$header .= '<th>'.get_lang('ElementList').'</th>';
$header .= '<th>'.get_lang('YourChoice').'</th>';
$header .= '<th>'.get_lang('ElementList').'</th>';
$header .= '<th>'.get_lang('YourChoice').'</th>';
if ($exercise->showExpectedChoice() || $exercise->showExpectedChoiceColumn()) {
$header .= '<th>'.get_lang('ExpectedChoice').'</th>';
}
$header .= '<th>'.get_lang('Status').'</th>';
+1 -1
View File
@@ -1050,7 +1050,7 @@ class Answer
}
// Fix correct answers
if (in_array($newQuestion->type, [DRAGGABLE, MATCHING, MATCHING_DRAGGABLE])) {
if (in_array($newQuestion->type, [DRAGGABLE, MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
$onlyAnswersFlip = array_flip($onlyAnswers);
foreach ($correctAnswers as $answer_id => $correct_answer) {
$params = [];
+34 -91
View File
@@ -1465,56 +1465,6 @@ class Exercise
}
}
/**
* changes the exercise sound file.
*
* @author Olivier Brouckaert
*
* @param string $sound - exercise sound file
* @param string $delete - ask to delete the file
*/
public function updateSound($sound, $delete)
{
global $audioPath, $documentPath;
$TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
if ($sound['size'] && (strstr($sound['type'], 'audio') || strstr($sound['type'], 'video'))) {
$this->sound = $sound['name'];
if (@move_uploaded_file($sound['tmp_name'], $audioPath.'/'.$this->sound)) {
$sql = "SELECT 1 FROM $TBL_DOCUMENT
WHERE
c_id = ".$this->course_id." AND
path = '".str_replace($documentPath, '', $audioPath).'/'.$this->sound."'";
$result = Database::query($sql);
if (!Database::num_rows($result)) {
$id = add_document(
$this->course,
str_replace($documentPath, '', $audioPath).'/'.$this->sound,
'file',
$sound['size'],
$sound['name']
);
api_item_property_update(
$this->course,
TOOL_DOCUMENT,
$id,
'DocumentAdded',
api_get_user_id()
);
item_property_update_on_folder(
$this->course,
str_replace($documentPath, '', $audioPath),
api_get_user_id()
);
}
}
} elseif ($delete && is_file($audioPath.'/'.$this->sound)) {
$this->sound = '';
}
}
/**
* changes the exercise type.
*
@@ -4516,7 +4466,7 @@ class Exercise
if (!$switchableAnswerSet) {
// not switchable answer, must be in the same place than teacher order
for ($i = 0; $i < count($listCorrectAnswers['words']); $i++) {
$studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
$studentAnswer = $choice[$i] ?? '';
$correctAnswer = $listCorrectAnswers['words'][$i];
if ($debug) {
@@ -4527,16 +4477,16 @@ class Exercise
// This value is the user input, not escaped while correct answer is escaped by ckeditor
// Works with cyrillic alphabet and when using ">" chars see #7718 #7610 #7618
// ENT_QUOTES is used in order to transform ' to &#039;
if (!$from_database) {
$studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
if ($debug) {
error_log('Student answer cleaned:');
error_log($studentAnswer);
}
//if (!$from_database) {
$studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
if ($debug) {
error_log('Student answer cleaned:');
error_log($studentAnswer);
}
//}
$isAnswerCorrect = 0;
if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database, true)) {
// gives the related weighting to the student
$questionScore += $answerWeighting[$i];
// increments total score
@@ -4594,7 +4544,7 @@ class Exercise
$found = false;
for ($j = 0; $j < count($listTeacherAnswerTemp); $j++) {
$correctAnswer = isset($listTeacherAnswerTemp[$j]) ? $listTeacherAnswerTemp[$j] : '';
$correctAnswer = $listTeacherAnswerTemp[$j] ?? '';
if (is_array($listTeacherAnswerTemp)) {
$correctAnswer = implode('||', $listTeacherAnswerTemp);
}
@@ -4995,7 +4945,7 @@ class Exercise
if (false === $this->showExpectedChoice() &&
false === $showTotalScoreAndUserChoicesInLastAttempt
) {
$user_answer = '';
$this->hideExpectedAnswer = true;
}
switch ($answerType) {
case MATCHING:
@@ -5057,9 +5007,6 @@ class Exercise
echo '</tr>';
break;
case DRAGGABLE:
if (false == $showTotalScoreAndUserChoicesInLastAttempt) {
$s_answer_label = '';
}
if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
break;
@@ -5067,35 +5014,15 @@ class Exercise
}
echo '<tr>';
if ($this->showExpectedChoice()) {
if (!in_array($this->results_disabled, [
RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
//RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
])
) {
echo '<td>'.$user_answer.'</td>';
} else {
$status = Display::label(get_lang('Correct'), 'success');
}
if ($this->showExpectedChoice() || $this->showExpectedChoiceColumn()) {
echo '<td>'.$s_answer_label.'</td>';
echo '<td>'.$user_answer.'</td>';
echo '<td>'.$real_list[$i_answer_correct_answer].'</td>';
echo '<td>'.$status.'</td>';
} else {
echo '<td>'.$s_answer_label.'</td>';
echo '<td>'.$user_answer.'</td>';
echo '<td>'.$counterAnswer.'</td>';
echo '<td>'.$status.'</td>';
echo '<td>';
if (in_array($answerType, [MATCHING, MATCHING_COMBINATION, MATCHING_DRAGGABLE, MATCHING_DRAGGABLE_COMBINATION])) {
if (isset($real_list[$i_answer_correct_answer]) &&
$showTotalScoreAndUserChoicesInLastAttempt === true
) {
echo Display::span(
$real_list[$i_answer_correct_answer],
['style' => 'color: #008000; font-weight: bold;']
);
}
}
echo '</td>';
}
echo '</tr>';
break;
@@ -10043,15 +9970,31 @@ class Exercise
);
} else {
if ($row['active'] == 0 || $visibility == 0) {
$visibility = Display::url(
Display::return_icon(
$visibleOnBaseCourse = api_get_item_visibility(
$courseInfo,
TOOL_QUIZ,
$row['iid'],
0
);
if ($visibleOnBaseCourse) {
$visibility = Display::url(
Display::return_icon(
'invisible.png',
get_lang('Activate'),
'',
ICON_SIZE_SMALL
),
'exercise.php?'.api_get_cidreq().'&choice=enable&sec_token='.$token.'&exerciseId='.$row['iid']
);
} else {
$visibility = Display::return_icon(
'invisible.png',
get_lang('Activate'),
'',
ICON_SIZE_SMALL
),
'exercise.php?'.api_get_cidreq().'&choice=enable&sec_token='.$token.'&exerciseId='.$row['iid']
);
);
}
} else {
// else if not active
$visibility = Display::url(
+38
View File
@@ -266,6 +266,22 @@ if (!empty($action) && $is_allowedToEdit) {
break;
}
if (!empty($sessionId)) {
$visibleOnBaseCourse = api_get_item_visibility(
$courseInfo,
TOOL_QUIZ,
$objExerciseTmp->iid,
0
);
if (!$visibleOnBaseCourse) {
Display::addFlash(Display::return_message(
sprintf(get_lang('CannotChangeVisibilityOfBaseCourseResourceX'), $objExerciseTmp->name),
'error'
));
break;
}
}
// enables an exercise
if (empty($sessionId)) {
$objExerciseTmp->enable();
@@ -368,6 +384,22 @@ if ($is_allowedToEdit) {
break;
}
if (!empty($sessionId)) {
$visibleOnBaseCourse = api_get_item_visibility(
$courseInfo,
TOOL_QUIZ,
$objExerciseTmp->iid,
0
);
if (!$visibleOnBaseCourse) {
Display::addFlash(Display::return_message(
sprintf(get_lang('CannotChangeVisibilityOfBaseCourseResourceX'), $objExerciseTmp->name),
'error'
));
break;
}
}
// Enables an exercise
if (empty($sessionId)) {
$objExerciseTmp->enable();
@@ -520,6 +552,12 @@ if ($is_allowedToEdit) {
// Teacher change exercise
break;
}
// Security: reject path traversal attempts (CWE-22)
if (!Security::check_abs_path($documentPath.$file, $documentPath.'/')) {
api_not_allowed(true);
}
// deletes an exercise
$imgparams = [];
$imgcount = 0;
+2 -2
View File
@@ -39,7 +39,7 @@ $TBL_USER = Database::get_main_table(TABLE_MAIN_USER);
$TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
$TBL_EXERCISES_QUESTION = Database::get_course_table(TABLE_QUIZ_QUESTION);
$TBL_TRACK_ATTEMPT_RECORDING = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING);
Display::display_header($nameTools, 'Exercise');
Display::display_header(get_lang('ViewHistoryChange'), 'Exercise');
if (isset($_GET['message'])) {
if (in_array($_GET['message'], ['ExerciseEdited'])) {
@@ -79,7 +79,7 @@ while ($row = Database::fetch_array($query)) {
echo '<td>'.$row['question'].'</td>';
echo '<td>'.$row['marks'].'</td>';
if (!empty($row['teacher_comment'])) {
echo '<td>'.$row['teacher_comment'].'</td>';
echo '<td>'.Security::remove_XSS($row['teacher_comment']).'</td>';
} else {
echo '<td>'.get_lang('WithoutComment').'</td>';
}
+5 -5
View File
@@ -491,12 +491,12 @@ class ExerciseResult
$filename = 'exercise_results_user_'.$user_id.'_'.$now.'.xlsx';
}
$spreadsheet = new PHPExcel();
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$spreadsheet->setActiveSheetIndex(0);
$worksheet = $spreadsheet->getActiveSheet();
$line = 1; // Skip first line
$column = 0; //skip the first column (row titles)
$line = 1;
$column = 1;
// check if exists column 'user'
$with_column_user = false;
@@ -584,7 +584,7 @@ class ExerciseResult
$line++;
foreach ($this->results as $row) {
$column = 0;
$column = 1;
if ($with_column_user) {
if (api_is_western_name_order()) {
$worksheet->setCellValueByColumnAndRow(
@@ -738,7 +738,7 @@ class ExerciseResult
}
$file = api_get_path(SYS_ARCHIVE_PATH).api_replace_dangerous_char($filename);
$writer = new PHPExcel_Writer_Excel2007($spreadsheet);
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
$writer->save($file);
DocumentManager::file_send_for_download($file, true, $filename);
+5 -6
View File
@@ -111,11 +111,10 @@ if (api_is_course_admin() && !in_array($origin, ['learnpath', 'embeddable', 'ifr
);
}
$exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
$learnpath_id = isset($exercise_stat_info['orig_lp_id']) ? $exercise_stat_info['orig_lp_id'] : 0;
$learnpath_item_id = isset($exercise_stat_info['orig_lp_item_id']) ? $exercise_stat_info['orig_lp_item_id'] : 0;
$learnpath_item_view_id = isset($exercise_stat_info['orig_lp_item_view_id'])
? $exercise_stat_info['orig_lp_item_view_id'] : 0;
$exerciseId = isset($exercise_stat_info['exe_exo_id']) ? $exercise_stat_info['exe_exo_id'] : 0;
$learnpath_id = $exercise_stat_info['orig_lp_id'] ?? 0;
$learnpath_item_id = $exercise_stat_info['orig_lp_item_id'] ?? 0;
$learnpath_item_view_id = $exercise_stat_info['orig_lp_item_view_id'] ?? 0;
$exerciseId = $exercise_stat_info['exe_exo_id'] ?? 0;
$logInfo = [
'tool' => TOOL_QUIZ,
@@ -357,7 +356,7 @@ $template->assign('actions', $pageActions);
$template->assign('content', $template->fetch($template->get_template('exercise/result.tpl')));
$template->display_one_col_template();
function showEmbeddableFinishButton()
function showEmbeddableFinishButton(): string
{
$js = '<script>
$(function () {
+7 -1
View File
@@ -974,9 +974,15 @@ if ('export' === $action) {
$content = Security::remove_XSS($content);
$includeOfficialCode = "";
if (true === api_get_configuration_value('quiz_result_pdf_export_include_official_code_in_file_name')) {
$includeOfficialCode = $user_info['official_code'].' ';
}
$params = [
'filename' => api_replace_dangerous_char(
$objExercise->name.' '.
$includeOfficialCode.
$user_info['complete_name'].' '.
api_get_local_time()
),
@@ -999,7 +1005,7 @@ if ('export' === $action) {
if (!is_dir($exportFolderPath)) {
@mkdir($exportFolderPath);
}
$pdfFileName = $user_info['firstname'].' '.$user_info['lastname'].'-attemptId'.$id.'.pdf';
$pdfFileName = $includeOfficialCode.$user_info['firstname'].' '.$user_info['lastname'].'-attemptId'.$id.'.pdf';
$pdfFileName = api_replace_dangerous_char($pdfFileName);
$fileNameToSave = $exportFolderPath.'/'.$pdfFileName;
$pdf->html_to_pdf_with_template($content, true, false, true, [], 'F', $fileNameToSave);
+6 -6
View File
@@ -123,11 +123,11 @@ $learnpath_item_view_id = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_R
$reminder = isset($_REQUEST['reminder']) ? (int) $_REQUEST['reminder'] : 0;
$remind_question_id = isset($_REQUEST['remind_question_id']) ? (int) $_REQUEST['remind_question_id'] : 0;
$exerciseId = isset($_REQUEST['exerciseId']) ? (int) $_REQUEST['exerciseId'] : 0;
$formSent = isset($_REQUEST['formSent']) ? $_REQUEST['formSent'] : null;
$exerciseResult = isset($_REQUEST['exerciseResult']) ? $_REQUEST['exerciseResult'] : null;
$exerciseResultCoordinates = isset($_REQUEST['exerciseResultCoordinates']) ? $_REQUEST['exerciseResultCoordinates'] : null;
$choice = isset($_REQUEST['choice']) ? $_REQUEST['choice'] : null;
$choice = empty($choice) ? isset($_REQUEST['choice2']) ? $_REQUEST['choice2'] : null : null;
$formSent = $_REQUEST['formSent'] ?? null;
$exerciseResult = $_REQUEST['exerciseResult'] ?? null;
$exerciseResultCoordinates = $_REQUEST['exerciseResultCoordinates'] ?? null;
$choice = $_REQUEST['choice'] ?? null;
$choice = empty($choice) ? $_REQUEST['choice2'] ?? null : null;
$current_question = $currentQuestionFromUrl = isset($_REQUEST['num']) ? (int) $_REQUEST['num'] : null;
$currentAnswer = isset($_REQUEST['num_answer']) ? (int) $_REQUEST['num_answer'] : null;
$logInfo = [
@@ -1081,7 +1081,7 @@ if (!api_is_allowed_to_session_edit()) {
}
$exercise_timeover = false;
$limit_time_exists = !empty($objExercise->start_time) || !empty($objExercise->end_time) ? true : false;
$limit_time_exists = !empty($objExercise->start_time) || !empty($objExercise->end_time);
if ($limit_time_exists) {
$exercise_start_time = api_strtotime($objExercise->start_time, 'UTC');
$exercise_end_time = api_strtotime($objExercise->end_time, 'UTC');
+2 -1
View File
@@ -688,7 +688,8 @@ function isQtiManifest($filePath)
*/
function qtiProcessManifest($filePath)
{
$xml = simplexml_load_file($filePath);
libxml_use_internal_errors(true);
$xml = simplexml_load_file($filePath, SimpleXMLElement::class, LIBXML_NONET);
$course = api_get_course_info();
$sessionId = api_get_session_id();
$courseDir = $course['path'];
+27 -15
View File
@@ -404,6 +404,8 @@ class FillBlanks extends Question
// remove starting and ending space and &nbsp;
$answer = api_preg_replace("/\xc2\xa0/", " ", $answer);
// remove invisible Unicode characters introduced by word processors
$answer = self::stripInvisibleChars($answer);
// start and end separator
$blankStartSeparator = self::getStartSeparator($form->getSubmitValue('select_separator'));
@@ -748,13 +750,13 @@ class FillBlanks extends Question
$listSeveral
);
//$studentAnswer = htmlspecialchars($studentAnswer);
$result = in_array($studentAnswer, $listSeveral);
$result = in_array(self::trimOption($studentAnswer), $listSeveral);
break;
case self::FILL_THE_BLANK_STANDARD:
default:
$correctAnswer = api_html_entity_decode($correctAnswer);
//$studentAnswer = htmlspecialchars($studentAnswer);
$result = $studentAnswer == self::trimOption($correctAnswer);
$result = self::trimOption($studentAnswer) == self::trimOption($correctAnswer);
break;
}
@@ -824,15 +826,14 @@ class FillBlanks extends Question
$listDetails = explode(':', $listArobaseSplit[0]);
// < number of item after the ::[score]:[size]:[separator_id]@ , here there are 3
$listWeightings = explode(',', $listDetails[0]);
if (count($listDetails) < 3) {
$listWeightings = explode(',', $listDetails[0]);
$listSizeOfInput = [];
for ($i = 0; $i < count($listWeightings); $i++) {
$listSizeOfInput[] = 200;
}
$blankSeparatorNumber = 0; // 0 is [...]
} else {
$listWeightings = explode(',', $listDetails[0]);
$listSizeOfInput = explode(',', $listDetails[1]);
$blankSeparatorNumber = $listDetails[2];
}
@@ -1258,8 +1259,6 @@ class FillBlanks extends Question
* @param int $feedbackType
* @param bool $resultsDisabled
* @param bool $showTotalScoreAndUserChoices
*
* @return string
*/
public static function getHtmlAnswer(
$answer,
@@ -1269,7 +1268,7 @@ class FillBlanks extends Question
$resultsDisabled = false,
$showTotalScoreAndUserChoices = false,
$exercise
) {
): string {
$hideExpectedAnswer = false;
$hideUserSelection = false;
if (!$exercise->showExpectedChoiceColumn()) {
@@ -1411,10 +1410,8 @@ class FillBlanks extends Question
* Check if a answer is correct by its text.
*
* @param string $answerText
*
* @return bool
*/
public static function isCorrect($answerText)
public static function isCorrect($answerText): bool
{
$answerInfo = self::getAnswerInfo($answerText, true);
$correctAnswerList = $answerInfo['words'];
@@ -1433,10 +1430,8 @@ class FillBlanks extends Question
* Clear the answer entered by student.
*
* @param string $answer
*
* @return string
*/
public static function clearStudentAnswer($answer)
public static function clearStudentAnswer($answer): string
{
$answer = htmlentities(api_utf8_encode($answer), ENT_QUOTES);
$answer = str_replace('&#039;', '&#39;', $answer); // fix apostrophe
@@ -1447,7 +1442,23 @@ class FillBlanks extends Question
}
/**
* Removes double spaces between words.
* Strips invisible/problematic Unicode characters introduced by word
* processors such as Microsoft Word.
* U+00A0 is normalised to a regular space; all others are removed.
*/
private static function stripInvisibleChars(string $text): string
{
$text = str_replace("\u{00A0}", ' ', $text); // Non-Breaking Space → space
return str_replace(
["\u{00AD}", "\u{200B}", "\u{200C}", "\u{200D}", "\u{2060}", "\u{FEFF}"],
'',
$text
);
}
/**
* Strips invisible characters and normalises whitespace.
*
* @param string $text
*
@@ -1456,7 +1467,8 @@ class FillBlanks extends Question
private static function trimOption($text)
{
$text = trim($text);
$text = self::stripInvisibleChars($text);
return preg_replace("/\s+/", ' ', $text);
return preg_replace("/\s+/", ' ', trim($text));
}
}
@@ -213,12 +213,12 @@ class HotpotatoesExerciseResult
$filename = 'exercise_results_user_'.$user_id.'_'.api_get_local_time().'.xls';
}
$spreadsheet = new PHPExcel();
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$spreadsheet->setActiveSheetIndex(0);
$worksheet = $spreadsheet->getActiveSheet();
$line = 0;
$column = 0; //skip the first column (row titles)
$line = 1;
$column = 1;
// check if exists column 'user'
$with_column_user = false;
@@ -335,7 +335,7 @@ class HotpotatoesExerciseResult
$line++;
foreach ($this->results as $row) {
$column = 0;
$column = 1;
if ($with_column_user) {
$worksheet->setCellValueByColumnAndRow(
@@ -426,7 +426,7 @@ class HotpotatoesExerciseResult
}
$file = api_get_path(SYS_ARCHIVE_PATH).api_replace_dangerous_char($filename);
$writer = new PHPExcel_Writer_Excel2007($spreadsheet);
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
$writer->save($file);
DocumentManager::file_send_for_download($file, true, $filename);
+12 -6
View File
@@ -13,8 +13,12 @@ use Symfony\Component\HttpFoundation\Request;
* @author Toon Keppens
*/
$modifyAnswers = (int) $_GET['hotspotadmin'];
if (!is_object($objQuestion)) {
if (!is_object($objQuestion) || empty($objQuestion->iid) || (int) $objQuestion->iid !== $modifyAnswers) {
$objQuestion = Question::read($modifyAnswers);
if (!$objQuestion) {
api_not_allowed();
}
Session::write('objQuestion', $objQuestion);
}
$questionName = $objQuestion->selectTitle();
@@ -330,14 +334,16 @@ if ($submitAnswers || $buttonBack) {
);
$objAnswer->save();
// sets the total weighting of the question
$objQuestion->updateWeighting($questionWeighting);
$objQuestion->iid = (int) $modifyAnswers;
$objQuestion->course = api_get_course_info();
$objQuestion->updateWeighting((float) $questionWeighting);
$objQuestion->save($objExercise);
$editQuestion = $questionId;
$editQuestion = $objQuestion->iid;
unset($modifyAnswers);
echo '<script type="text/javascript">window.location.href="'.$hotspot_admin_url
.'&message=ItemUpdated"</script>';
echo '<script type="text/javascript">window.location.href="'.
$hotspot_admin_url.'&message=ItemUpdated"</script>';
}
}
}
+3
View File
@@ -337,6 +337,7 @@ if (!empty($attempts)) {
RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
RESULT_DISABLE_RANKING,
RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
RESULT_DISABLE_RADAR,
]
)) {
$row['result'] = $score;
@@ -353,6 +354,7 @@ if (!empty($attempts)) {
RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
RESULT_DISABLE_RANKING,
RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
RESULT_DISABLE_RADAR,
]
) || (
$objExercise->results_disabled == RESULT_DISABLE_SHOW_SCORE_ONLY &&
@@ -419,6 +421,7 @@ if (!empty($attempts)) {
case RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES:
case RESULT_DISABLE_RANKING:
case RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER:
case RESULT_DISABLE_RADAR:
$header_names = [
get_lang('Attempt'),
get_lang('StartDate'),
+48 -15
View File
@@ -16,6 +16,8 @@ $statusId = isset($_REQUEST['status']) ? (int) $_REQUEST['status'] : 0;
$questionTypeId = isset($_REQUEST['questionTypeId']) ? (int) $_REQUEST['questionTypeId'] : 0;
$exportXls = isset($_REQUEST['export_xls']) && !empty($_REQUEST['export_xls']) ? (int) $_REQUEST['export_xls'] : 0;
$action = $_REQUEST['a'] ?? null;
$startDate = isset($_REQUEST['start_date']) ? $_REQUEST['start_date'] : '';
$endDate = isset($_REQUEST['end_date']) ? $_REQUEST['end_date'] : '';
api_block_anonymous_users();
@@ -170,6 +172,14 @@ $htmlHeadXtra[] = '<script>
}
</script>';
$htmlHeadXtra[] = '<script>
$(function() {
$(".datepicker").datepicker({
dateFormat: "yy-mm-dd"
});
});
</script>';
if ($exportXls) {
ExerciseLib::exportPendingAttemptsToExcel($_REQUEST);
}
@@ -286,6 +296,22 @@ $form->addSelect(
]
);
$userOptions = [];
if (!empty($filter_user)) {
$userInfo = api_get_user_info($filter_user);
if (!empty($userInfo)) {
$userOptions[$filter_user] = $userInfo['complete_name_with_username'];
}
}
$form->addSelectAjax(
'filter_by_user',
get_lang('User'),
$userOptions,
[
'url' => api_get_path(WEB_AJAX_PATH).'user_manager.ajax.php?a=get_user_like',
]
);
$status = [
1 => get_lang('All'),
2 => get_lang('Validated'),
@@ -293,16 +319,27 @@ $status = [
4 => get_lang('Unclosed'),
5 => get_lang('Ongoing'),
];
$form->addSelect('status', get_lang('Status'), $status);
$questionType = [
0 => get_lang('All'),
1 => get_lang('QuestionsWithNoAutomaticCorrection'),
];
$form->addSelect('questionTypeId', get_lang('QuestionType'), $questionType);
$form->addElement(
'text',
'start_date',
get_lang('StartDate'),
['id' => 'start_date', 'class' => 'datepicker', 'autocomplete' => 'off', 'style' => 'width:120px']
);
$form->addElement(
'text',
'end_date',
get_lang('EndDate'),
['id' => 'end_date', 'class' => 'datepicker', 'autocomplete' => 'off', 'style' => 'width:120px']
);
$form->addButtonSearch(get_lang('Search'), 'pendingSubmit');
$content = $form->returnForm();
@@ -315,7 +352,9 @@ if (empty($statusId)) {
$url = api_get_path(WEB_AJAX_PATH).
'model.ajax.php?a=get_exercise_pending_results&filter_by_user='.$filter_user.
'&course_id='.$courseId.'&exercise_id='.$exerciseId.'&status='.$statusId.'&questionType='.$questionTypeId.'&showAttemptsInSessions='.$showAttemptsInSessions;
'&course_id='.$courseId.'&exercise_id='.$exerciseId.'&status='.$statusId.'&questionType='.$questionTypeId.
'&showAttemptsInSessions='.$showAttemptsInSessions.
'&start_date='.$startDate.'&end_date='.$endDate;
$action_links = '';
$officialCodeInList = api_get_setting('show_official_code_exercise_result_list');
@@ -375,16 +414,6 @@ $column_model = [
'align' => 'left',
'search' => 'false',
'sortable' => 'false',
//'stype' => 'select',
//for the bottom bar
/*'searchoptions' => [
'defaultValue' => '',
'value' => ':'.get_lang('All').';1:'.get_lang('Validated').';0:'.get_lang('NotValidated'),
],*/
//for the top bar
/*'editoptions' => [
'value' => ':'.get_lang('All').';1:'.get_lang('Validated').';0:'.get_lang('NotValidated'),
],*/
],
[
'name' => 'qualificator_fullname',
@@ -432,8 +461,12 @@ function action_formatter(cellvalue, options, rowObject) {
return "<span title=\""+tabLoginx[0]+rowObject[2]+tabLoginx[1]+"\">"+cellvalue+"</span>";
}';
$extra_params['autowidth'] = 'true';
$extra_params['height'] = 'auto';
$extra_params = [
'autowidth' => 'true',
'height' => 'auto',
'sortname' => 'exe_date',
'sortorder' => 'asc',
];
$gridJs = Display::grid_js(
'results',
$url,
+3
View File
@@ -66,6 +66,9 @@ if ($student_id === $current_user_id && ExerciseSignaturePlugin::exerciseHasSign
}
}
if (RESULT_DISABLE_RADAR === (int) $objExercise->results_disabled) {
$htmlHeadXtra[] = api_get_js('chartjs/Chart.min.js');
}
$htmlHeadXtra[] = '<link rel="stylesheet" href="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/css/hotspot.css">';
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/js/hotspot.js"></script>';
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'annotation/js/annotation.js"></script>';
+10 -3
View File
@@ -16,15 +16,20 @@ $_user = api_get_user_info();
$this_section = SECTION_COURSES;
$documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path']."/document";
$test = $_REQUEST['test'];
$test = $_REQUEST['test'] ?? '';
$full_file_path = $documentPath.$test;
$fileToDelete = $full_file_path.$_user['user_id'].".t.html";
my_delete($full_file_path.$_user['user_id'].".t.html");
if (!Security::check_abs_path($fileToDelete, $documentPath.'/')) {
api_not_allowed(true);
}
my_delete($fileToDelete);
$TABLETRACK_HOTPOTATOES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTPOTATOES);
$TABLE_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
$score = $_REQUEST['score'];
$score = isset($_REQUEST['score']) ? Security::remove_XSS($_REQUEST['score']) : '';
$origin = api_get_origin();
$learnpath_item_id = intval($_REQUEST['learnpath_item_id']);
$lpViewId = isset($_REQUEST['lp_view_id']) ? intval($_REQUEST['lp_view_id']) : null;
@@ -45,6 +50,8 @@ function save_scores($file, $score)
global $origin;
$TABLETRACK_HOTPOTATOES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTPOTATOES);
$_user = api_get_user_info();
$file = Security::remove_XSS($file);
$score = intval($score);
// if tracking is disabled record nothing
$weighting = 100; // 100%
$date = api_get_utc_datetime();
+9 -2
View File
@@ -28,7 +28,14 @@ $lpViewId = isset($_REQUEST['lp_view_id']) ? $_REQUEST['lp_view_id'] : null;
$user_id = api_get_user_id();
$full_file_path = $document_path.$doc_url;
my_delete($full_file_path.$user_id.'.t.html');
// Security: reject path traversal attempts (CWE-22)
if (!Security::check_abs_path($full_file_path, $document_path.'/')) {
api_not_allowed(true);
}
$fileToDelete = $full_file_path.$user_id.'.t.html';
my_delete($fileToDelete);
$content = ReadFileCont($full_file_path.$user_id.'.t.html');
if ($content == '') {
@@ -95,7 +102,7 @@ $htmlHeadXtra[] = <<<HTML
});
iframe.height = maxheight;
}
$(function() {
var iframe = document.getElementById('hotpotatoe');
iframe.onload = function () {
+9 -9
View File
@@ -160,7 +160,7 @@ function lp_upload_quiz_action_handling()
$questionTypeList = [];
$answerList = [];
$quizTitle = '';
$objPHPExcel = PHPExcel_IOFactory::load($_FILES['user_upload_quiz']['tmp_name']);
$objPHPExcel = \PhpOffice\PhpSpreadsheet\IOFactory::load($_FILES['user_upload_quiz']['tmp_name']);
$objPHPExcel->setActiveSheetIndex(0);
$worksheet = $objPHPExcel->getActiveSheet();
$highestRow = $worksheet->getHighestRow(); // e.g. 10
@@ -171,9 +171,9 @@ function lp_upload_quiz_action_handling()
$useCustomScore = isset($_POST['user_custom_score']) ? true : false;
for ($row = 1; $row <= $highestRow; $row++) {
$cellTitleInfo = $worksheet->getCellByColumnAndRow(0, $row);
$cellDataInfo = $worksheet->getCellByColumnAndRow(1, $row);
$cellScoreInfo = $worksheet->getCellByColumnAndRow(2, $row);
$cellTitleInfo = $worksheet->getCellByColumnAndRow(1, $row);
$cellDataInfo = $worksheet->getCellByColumnAndRow(2, $row);
$cellScoreInfo = $worksheet->getCellByColumnAndRow(3, $row);
$title = $cellTitleInfo->getValue();
switch ($title) {
@@ -188,9 +188,9 @@ function lp_upload_quiz_action_handling()
$answerIndex = 0;
while ($continue) {
$answerRow++;
$answerInfoTitle = $worksheet->getCellByColumnAndRow(0, $answerRow);
$answerInfoData = $worksheet->getCellByColumnAndRow(1, $answerRow);
$answerInfoExtra = $worksheet->getCellByColumnAndRow(2, $answerRow);
$answerInfoTitle = $worksheet->getCellByColumnAndRow(1, $answerRow);
$answerInfoData = $worksheet->getCellByColumnAndRow(2, $answerRow);
$answerInfoExtra = $worksheet->getCellByColumnAndRow(3, $answerRow);
$answerInfoTitle = $answerInfoTitle->getValue();
if (strpos($answerInfoTitle, 'Answer') !== false) {
$answerList[$numberQuestions][$answerIndex]['data'] = $answerInfoData->getValue();
@@ -212,8 +212,8 @@ function lp_upload_quiz_action_handling()
$questionTypeIndex = 0;
while ($continue) {
$answerRow++;
$questionTypeTitle = $worksheet->getCellByColumnAndRow(0, $answerRow);
$questionTypeExtra = $worksheet->getCellByColumnAndRow(2, $answerRow);
$questionTypeTitle = $worksheet->getCellByColumnAndRow(1, $answerRow);
$questionTypeExtra = $worksheet->getCellByColumnAndRow(3, $answerRow);
$title = $questionTypeTitle->getValue();
if ($title === 'QuestionType') {
$questionTypeList[$numberQuestions] = $questionTypeExtra->getValue();
+11
View File
@@ -9,6 +9,17 @@ api_protect_course_script(true);
api_block_anonymous_users();
GradebookUtils::block_students();
if (isset($_GET['import'])) {
$queryString = $_SERVER['QUERY_STRING'];
$webPath = api_get_path(WEB_CODE_PATH);
$newUrl = $webPath.'gradebook/gradebook_view_result.php';
if (!empty($queryString)) {
$newUrl .= '?'.$queryString;
}
header("Location: $newUrl");
exit;
}
$selectEval = isset($_GET['selecteval']) ? (int) $_GET['selecteval'] : 0;
$resultadd = new Result();
+19 -2
View File
@@ -10,8 +10,19 @@ api_block_anonymous_users();
GradebookUtils::block_students();
$evaledit = Evaluation::load($_GET['editeval']);
if ($evaledit[0]->is_locked() && !api_is_platform_admin()) {
api_not_allowed();
if (empty($evaledit[0])) {
api_not_allowed(true);
}
if (!api_is_platform_admin()) {
$currentCourseCode = api_get_course_id();
if ($evaledit[0]->get_course_code() && $evaledit[0]->get_course_code() != $currentCourseCode) {
api_not_allowed(true);
}
if ($evaledit[0]->is_locked()) {
api_not_allowed(true);
}
}
$form = new EvalForm(
EvalForm::TYPE_EDIT,
@@ -23,6 +34,12 @@ $form = new EvalForm(
);
if ($form->validate()) {
$values = $form->exportValues();
$evaluationId = (int) $values['hid_id'];
if ($evaluationId !== (int) $evaledit[0]->get_id()) {
api_not_allowed(true);
}
$eval = new Evaluation();
$eval->set_id($values['hid_id']);
$eval->set_name($values['name']);
+11 -5
View File
@@ -10,12 +10,18 @@ $current_course_tool = TOOL_GRADEBOOK;
api_protect_course_script(true);
api_block_anonymous_users();
$currentUserId = api_get_user_id();
$sessionId = api_get_session_id();
$isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh(
api_get_user_id(),
$currentUserId,
api_get_course_info()
);
if (!$isDrhOfCourse) {
$isDrhOfSession = $sessionId && !empty(SessionManager::getSessionFollowedByDrh($currentUserId, $sessionId));
if (!$isDrhOfCourse && !$isDrhOfSession) {
GradebookUtils::block_students();
}
@@ -75,7 +81,7 @@ $simple_search_form = new UserForm(
$values = $simple_search_form->exportValues();
$keyword = '';
if (isset($_GET['search']) && !empty($_GET['search'])) {
if (!empty($_GET['search'])) {
$keyword = Security::remove_XSS($_GET['search']);
}
if ($simple_search_form->validate() && empty($keyword)) {
@@ -90,7 +96,7 @@ if (!empty($keyword)) {
$users = GradebookUtils::get_all_users($alleval, $alllinks);
}
}
$offset = isset($_GET['offset']) ? $_GET['offset'] : '0';
$offset = $_GET['offset'] ?? '0';
$addparams = ['selectcat' => $cat[0]->get_id()];
if (isset($_GET['search'])) {
@@ -128,7 +134,7 @@ if (isset($_GET['export_pdf']) && 'category' === $_GET['export_pdf']) {
$params['join_firstname_lastname'] = true;
$params['show_official_code'] = true;
$params['export_pdf'] = true;
if ($cat[0]->is_locked() == true || api_is_platform_admin()) {
if ($cat[0]->is_locked() || api_is_platform_admin()) {
Display::set_header(null, false, false);
GradebookUtils::export_pdf_flatview(
$flatViewTable,
+15 -7
View File
@@ -40,7 +40,7 @@ if ($eval[0]->get_category_id() < 0) {
//load the result with the evaluation id
if (isset($_GET['delete_mark'])) {
$result = Result::load($_GET['delete_mark']);
if (!empty($result[0])) {
if (!empty($result[0]) && $result[0]->get_evaluation_id() == $select_eval) {
$result[0]->delete();
}
}
@@ -56,7 +56,9 @@ if (isset($_GET['action'])) {
switch ($_GET['action']) {
case 'delete_attempt':
$result = Result::load($_GET['editres']);
if ($allowMultipleAttempts && !empty($result) && isset($result[0]) && api_is_allowed_to_edit()) {
if ($allowMultipleAttempts && !empty($result) && isset($result[0]) && api_is_allowed_to_edit()
&& $result[0]->get_evaluation_id() == $select_eval
) {
/** @var Result $result */
$result = $result[0];
$url = api_get_self().'?selecteval='.$select_eval.'&'.api_get_cidreq().'&editres='.$result->get_id();
@@ -74,7 +76,9 @@ if (isset($_GET['action'])) {
break;
case 'add_attempt':
$result = Result::load($_GET['editres']);
if ($allowMultipleAttempts && !empty($result) && isset($result[0]) && api_is_allowed_to_edit()) {
if ($allowMultipleAttempts && !empty($result) && isset($result[0]) && api_is_allowed_to_edit()
&& $result[0]->get_evaluation_id() == $select_eval
) {
/** @var Result $result */
$result = $result[0];
$backUrl = api_get_self().'?selecteval='.$select_eval.'&'.api_get_cidreq();
@@ -512,8 +516,10 @@ if (isset($_GET['export'])) {
if (isset($_GET['resultdelete'])) {
$result = Result::load($_GET['resultdelete']);
$result[0]->delete();
Display::addFlash(Display::return_message(get_lang('ResultDeleted')));
if (!empty($result[0]) && $result[0]->get_evaluation_id() == $select_eval) {
$result[0]->delete();
Display::addFlash(Display::return_message(get_lang('ResultDeleted')));
}
header('Location: gradebook_view_result.php?selecteval='.$select_eval.'&'.api_get_cidreq());
exit;
}
@@ -534,8 +540,10 @@ if (isset($_POST['action'])) {
$number_of_deleted_results = 0;
foreach ($_POST['id'] as $indexstr) {
$result = Result::load($indexstr);
$result[0]->delete();
$number_of_deleted_results++;
if (!empty($result[0]) && $result[0]->get_evaluation_id() == $select_eval) {
$result[0]->delete();
$number_of_deleted_results++;
}
}
Display::addFlash(Display::return_message(get_lang('ResultsDeleted'), 'confirmation', false));
header('Location: gradebook_view_result.php?massdelete=&selecteval='.$select_eval.'&'.api_get_cidreq());
+2 -2
View File
@@ -973,8 +973,8 @@ class GradebookUtils
}
/**
* @param FlatViewTable $flatviewtable
* @param Category $cat
* @param FlatViewTable $flatviewtable
* @param array<int, Category> $cat
* @param $users
* @param $alleval
* @param $alllinks
+10 -5
View File
@@ -633,18 +633,23 @@ abstract class AbstractLink implements GradebookItem
*/
public static function getCurrentUserRanking($userId, $studentList)
{
$previousScore = null;
$ranking = null;
$position = null;
$currentUserId = $userId;
if (!empty($studentList) && !empty($currentUserId)) {
$studentList = array_map('floatval', $studentList);
asort($studentList);
$ranking = $count = count($studentList);
foreach ($studentList as $userId => $position) {
arsort($studentList);
$count = count($studentList);
foreach ($studentList as $userId => $score) {
$position++;
if ($previousScore === null || $score < $previousScore) {
$ranking = $position;
}
$previousScore = $score;
if ($currentUserId == $userId) {
break;
}
$ranking--;
}
// If no ranking was detected.
+7 -13
View File
@@ -128,12 +128,9 @@ class Category implements GradebookItem
return $this->weight;
}
/**
* @return bool
*/
public function is_locked()
public function is_locked(): bool
{
return isset($this->locked) && $this->locked == 1 ? true : false;
return isset($this->locked) && $this->locked == 1;
}
/**
@@ -429,7 +426,7 @@ class Category implements GradebookItem
* @param bool $order_by Whether to show all "session"
* categories (true) or hide them (false) in case there is no session id
*
* @return array
* @return array<int, Category>
*/
public static function load(
$id = null,
@@ -439,7 +436,7 @@ class Category implements GradebookItem
$visible = null,
$session_id = null,
$order_by = null
) {
): array {
//if the category given is explicitly 0 (not null), then create
// a root category object (in memory)
if (isset($id) && (int) $id === 0) {
@@ -2681,10 +2678,7 @@ class Category implements GradebookItem
return $this->weight - $subWeight;
}
/**
* @return Category
*/
private static function create_root_category()
private static function create_root_category(): Category
{
$cat = new Category();
$cat->set_id(0);
@@ -2704,9 +2698,9 @@ class Category implements GradebookItem
/**
* @param Doctrine\DBAL\Driver\Statement|null $result
*
* @return array
* @return array<int, Category>
*/
private static function create_category_objects_from_sql_result($result)
private static function create_category_objects_from_sql_result($result): array
{
$categories = [];
$allow = api_get_configuration_value('allow_gradebook_stats');
+4 -4
View File
@@ -221,7 +221,7 @@ class Evaluation implements GradebookItem
* @param int $category_id parent category
* @param int $visible visible
*
* @return array
* @return array<int, Evaluation>
*/
public static function load(
$id = null,
@@ -230,7 +230,7 @@ class Evaluation implements GradebookItem
$category_id = null,
$visible = null,
$locked = null
) {
): array {
$table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_EVALUATION);
$sql = 'SELECT * FROM '.$table;
$paramcount = 0;
@@ -935,9 +935,9 @@ class Evaluation implements GradebookItem
/**
* @param array $result
*
* @return array
* @return array<int, Evaluation>
*/
private static function create_evaluation_objects_from_sql_result($result)
private static function create_evaluation_objects_from_sql_result($result): array
{
$alleval = [];
$allow = api_get_configuration_value('allow_gradebook_stats');
+14 -14
View File
@@ -93,7 +93,7 @@ class ForumThreadLink extends AbstractLink
$sql = "SELECT count(*) AS number FROM $table
WHERE
c_id = ".$this->course_id." AND
thread_id = '".$this->get_ref_id()."'
thread_id = '".$this->get_ref_id()."'
";
$result = Database::query($sql);
$number = Database::fetch_row($result);
@@ -122,8 +122,8 @@ class ForumThreadLink extends AbstractLink
$sql = 'SELECT thread_qualify_max
FROM '.Database::get_course_table(TABLE_FORUM_THREAD)."
WHERE
c_id = ".$this->course_id." AND
WHERE
c_id = ".$this->course_id." AND
thread_id = '".$this->get_ref_id()."'
$sessionCondition
";
@@ -131,8 +131,8 @@ class ForumThreadLink extends AbstractLink
$assignment = Database::fetch_array($query);
$sql = "SELECT * FROM $thread_qualify
WHERE
c_id = ".$this->course_id." AND
WHERE
c_id = ".$this->course_id." AND
thread_id = ".$this->get_ref_id()."
$sessionCondition
";
@@ -265,9 +265,9 @@ class ForumThreadLink extends AbstractLink
{
$sessionId = $this->get_session_id();
$sql = 'SELECT count(id) from '.$this->get_forum_thread_table().'
WHERE
c_id = '.$this->course_id.' AND
thread_id = '.$this->get_ref_id().' AND
WHERE
c_id = '.$this->course_id.' AND
thread_id = '.$this->get_ref_id().' AND
session_id='.$sessionId;
$result = Database::query($sql);
$number = Database::fetch_row($result);
@@ -281,8 +281,8 @@ class ForumThreadLink extends AbstractLink
//it was extracts the forum id
$sql = 'SELECT * FROM '.$this->get_forum_thread_table()."
WHERE
c_id = '.$this->course_id.' AND
thread_id = '".$this->get_ref_id()."' AND
c_id = ".$this->course_id." AND
thread_id = ".$this->get_ref_id()." AND
session_id = $sessionId ";
$result = Database::query($sql);
$row = Database::fetch_array($result, 'ASSOC');
@@ -304,7 +304,7 @@ class ForumThreadLink extends AbstractLink
$ref_id = $this->get_ref_id();
if (!empty($ref_id)) {
$sql = 'UPDATE '.$this->get_forum_thread_table().' SET
$sql = 'UPDATE '.$this->get_forum_thread_table().' SET
thread_weight='.api_float_val($weight).'
WHERE c_id = '.$this->course_id.' AND thread_id= '.$ref_id;
Database::query($sql);
@@ -344,9 +344,9 @@ class ForumThreadLink extends AbstractLink
if (!isset($this->exercise_data)) {
$sql = 'SELECT * FROM '.$this->get_forum_thread_table().'
WHERE
c_id = '.$this->course_id.' AND
thread_id = '.$this->get_ref_id().' AND
WHERE
c_id = '.$this->course_id.' AND
thread_id = '.$this->get_ref_id().' AND
'.$session_condition;
$query = Database::query($sql);
$this->exercise_data = Database::fetch_array($query);
+2 -2
View File
@@ -85,9 +85,9 @@ class Result
* @param $user_id user id (student)
* @param $evaluation_id evaluation where this is a result for
*
* @return array
* @return array<int, Result>
*/
public static function load($id = null, $user_id = null, $evaluation_id = null)
public static function load($id = null, $user_id = null, $evaluation_id = null): array
{
$tbl_user = Database::get_main_table(TABLE_MAIN_USER);
$tbl_grade_results = Database::get_main_table(TABLE_MAIN_GRADEBOOK_RESULT);
+15 -4
View File
@@ -429,6 +429,7 @@ class DisplayGradebook
}
// for course admin & platform admin add item buttons are added to the header
$toolbarActions = [];
$actionsLeft = '';
$actionsRight = '';
$my_api_cidreq = api_get_cidreq();
@@ -562,11 +563,13 @@ class DisplayGradebook
}
$isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh(
api_get_user_id(),
$userId,
api_get_course_info()
);
if ($isDrhOfCourse) {
$isDrhOfSession = $sessionId && !empty(SessionManager::getSessionFollowedByDrh($userId, $sessionId));
if ($isDrhOfCourse || $isDrhOfSession) {
$actionsLeft .= '<a href="gradebook_flatview.php?'.$my_api_cidreq.'&selectcat='.$catobj->get_id().'">'
.Display::return_icon(
'statistics.png',
@@ -578,9 +581,17 @@ class DisplayGradebook
}
if ($isCoach || api_is_allowed_to_edit(null, true)) {
echo $toolbar = Display::toolbarAction(
$toolbarActions = [$actionsLeft, $actionsRight];
}
if (empty($toolbarActions) && ($isDrhOfCourse || $isDrhOfSession)) {
$toolbarActions = [$actionsLeft];
}
if ($toolbarActions) {
echo Display::toolbarAction(
'gradebook-actions',
[$actionsLeft, $actionsRight]
$toolbarActions
);
}
+8 -11
View File
@@ -21,11 +21,11 @@ class GradeBookResult
/**
* Exports the complete report as a CSV file.
*
* @param string $dato Document path inside the document tool
* @param array $dato Document path inside the document tool
*
* @return bool False on error
*/
public function exportCompleteReportCSV($dato)
public function exportCompleteReportCSV(array $dato)
{
$filename = 'gradebook_results_'.gmdate('YmdGis').'.csv';
$data = '';
@@ -80,19 +80,16 @@ class GradeBookResult
* Exports the complete report as an XLS file.
*
* @param array $data
*
* @throws PHPExcel_Exception
* @throws PHPExcel_Writer_Exception
*/
public function exportCompleteReportXLS($data)
{
$filename = 'gradebook-results-'.api_get_local_time().'.xlsx';
$spreadsheet = new PHPExcel();
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$spreadsheet->setActiveSheetIndex(0);
$worksheet = $spreadsheet->getActiveSheet();
$line = 1;
$column = 0;
$column = 1;
// headers.
foreach ($data[0] as $headerData) {
$title = $headerData;
@@ -100,7 +97,7 @@ class GradeBookResult
$title = $headerData['header'];
}
$title = html_entity_decode(strip_tags($title));
$worksheet->SetCellValueByColumnAndRow(
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
$title
@@ -110,9 +107,9 @@ class GradeBookResult
$line++;
$cant_students = count($data[1]);
for ($i = 0; $i < $cant_students; $i++) {
$column = 0;
$column = 1;
foreach ($data[1][$i] as $col_name) {
$worksheet->SetCellValueByColumnAndRow(
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
html_entity_decode(strip_tags($col_name))
@@ -123,7 +120,7 @@ class GradeBookResult
}
$file = api_get_path(SYS_ARCHIVE_PATH).api_replace_dangerous_char($filename);
$writer = new PHPExcel_Writer_Excel2007($spreadsheet);
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
$writer->save($file);
DocumentManager::file_send_for_download($file, true, $filename);
exit;
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+4 -37
View File
@@ -145,43 +145,10 @@ switch ($action) {
break;
case 'search_course':
if (api_is_teacher() || api_is_platform_admin()) {
if (isset($_GET['session_id']) && !empty($_GET['session_id'])) {
//if session is defined, lets find only courses of this session
$courseList = SessionManager::get_course_list_by_session_id(
$_GET['session_id'],
$_GET['q']
);
} else {
//if session is not defined lets search all courses STARTING with $_GET['q']
//TODO change this function to search not only courses STARTING with $_GET['q']
if (api_is_platform_admin()) {
$courseList = CourseManager::get_courses_list(
0,
0,
'title',
'ASC',
-1,
$_GET['q'],
null,
true
);
} elseif (api_is_teacher()) {
$courseList = CourseManager::get_course_list_of_user_as_course_admin(api_get_user_id(), $_GET['q']);
$category = api_get_configuration_value('course_category_code_to_use_as_model');
if (!empty($category)) {
$alreadyAdded = [];
if (!empty($courseList)) {
$alreadyAdded = array_column($courseList, 'id');
}
$coursesInCategory = CourseCategory::getCoursesInCategory($category, $_GET['q']);
foreach ($coursesInCategory as $course) {
if (!in_array($course['id'], $alreadyAdded)) {
$courseList[] = $course;
}
}
}
}
}
$courseList = CourseManager::searchCourse(
$_REQUEST['q'],
isset($_GET['session_id']) ? (int) $_GET['session_id'] : 0
);
$results = [];
if (empty($courseList)) {
+12 -9
View File
@@ -1,6 +1,7 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CourseBundle\Entity\CCourseDescription;
use Chamilo\CourseBundle\Entity\CTool;
use ChamiloSession as Session;
@@ -290,15 +291,17 @@ switch ($action) {
echo get_lang('PrivateAccess');
break;
}
$table = Database::get_course_table(TABLE_COURSE_DESCRIPTION);
$sql = "SELECT * FROM $table
WHERE c_id = ".$course_info['real_id']." AND session_id = 0
ORDER BY id";
$result = Database::query($sql);
if (Database::num_rows($result) > 0) {
while ($description = Database::fetch_object($result)) {
$descriptions[$description->id] = $description;
}
/** @var array<int, CCourseDescription> $courseDescriptions */
$courseDescriptions = Database::getManager()
->getRepository(CCourseDescription::class)
->findBy(['cId' => $course_info['real_id'], 'sessionId' => 0])
;
$descriptions = [];
foreach ($courseDescriptions as $courseDescription) {
$descriptions[$courseDescription->getIid()] = $courseDescription;
// Function that displays the details of the course description in html.
$content = CourseManager::get_details_course_description_html(
$descriptions,
+32 -1
View File
@@ -6,6 +6,7 @@
*/
use Chamilo\CoreBundle\Component\Editor\Driver\Driver;
use Chamilo\CoreBundle\Component\Editor\Driver\PersonalDriver;
require_once __DIR__.'/../global.inc.php';
@@ -217,6 +218,20 @@ switch ($action) {
$data = [];
$fileUpload = $_FILES['upload'];
try {
new Image($fileUpload['tmp_name']);
} catch (Exception $e) {
echo json_encode([
'uploaded' => 0,
'error' => [
'message' => get_lang('MissingImagesDetected'),
],
]);
exit;
}
$mimeType = mime_content_type($fileUpload['tmp_name']);
$isMimeAccepted = (new Driver())->mimeAccepted($mimeType, ['image']);
@@ -225,6 +240,22 @@ switch ($action) {
exit;
}
try {
$fileUpload['size'] = DocumentManager::autoResizeImageIfNeeded(
$fileUpload['size'],
$fileUpload['tmp_name']
);
} catch (Exception $e) {
echo json_encode([
'uploaded' => 0,
'error' => [
'message' => $e->getMessage(),
],
]);
exit;
}
$isAllowedToEdit = api_is_allowed_to_edit(null, true);
if ($isAllowedToEdit) {
$globalFile = ['files' => $fileUpload];
@@ -257,7 +288,7 @@ switch ($action) {
mkdir($syspath, api_get_permissions_for_new_directories(), true);
}
$webpath = UserManager::getUserPathById($userId, 'web').'my_files';
$fileUploadName = $fileUpload['name'];
$fileUploadName = disable_dangerous_file(api_replace_dangerous_char($fileUpload['name']));
if (file_exists($syspath.$fileUploadName)) {
$extension = pathinfo($fileUploadName, PATHINFO_EXTENSION);
$fileName = pathinfo($fileUploadName, PATHINFO_FILENAME);
+12 -1
View File
@@ -56,6 +56,10 @@ switch ($action) {
}
break;*/
case 'export_all_certificates':
if (!api_is_allowed_to_edit() && !api_is_student_boss()) {
exit;
}
$categoryId = (int) $_GET['cat_id'];
$filterOfficialCodeGet = isset($_GET['filter']) ? Security::remove_XSS($_GET['filter']) : null;
@@ -76,7 +80,14 @@ switch ($action) {
$userList = implode(',', $userList);
shell_exec("php $commandScript $courseCode $sessionId $categoryId $userList > /dev/null &");
shell_exec(sprintf(
"php %s %s %s %s %s > /dev/null &",
escapeshellarg($commandScript),
escapeshellarg($courseCode),
escapeshellarg((string) $sessionId),
escapeshellarg((string) $categoryId),
escapeshellarg($userList)
));
break;
case 'verify_export_all_certificates':
$categoryId = (int) $_GET['cat_id'];
+14 -8
View File
@@ -251,10 +251,6 @@ if (($search || $forceSearch) && ($search !== 'false')) {
}
$whereCondition .= $extraQuestionCondition;
if (isset($filters->custom_dates)) {
$whereCondition .= $filters->custom_dates;
}
}
} elseif (!empty($filters->rules)) {
$whereCondition .= ' AND ( ';
@@ -643,17 +639,19 @@ switch ($action) {
true
);
break;
case 'get_exercise_pending_results':
if ((false === api_is_teacher()) && (false === api_is_session_admin())) {
exit;
}
$search_start_date = isset($_REQUEST['start_date']) && !empty($_REQUEST['start_date']) ? $_REQUEST['start_date'] : null;
$search_end_date = isset($_REQUEST['end_date']) && !empty($_REQUEST['end_date']) ? $_REQUEST['end_date'] : null;
$courseId = $_REQUEST['course_id'] ?? 0;
$exerciseId = $_REQUEST['exercise_id'] ?? 0;
$status = $_REQUEST['status'] ?? 0;
$questionType = $_REQUEST['questionType'] ?? 0;
$showAttemptsInSessions = (bool) $_REQUEST['showAttemptsInSessions'];
if (!empty($_GET['filter_by_user'])) {
$showAttemptsInSessions = $_REQUEST['showAttemptsInSessions'] ? true : false;
if (isset($_GET['filter_by_user']) && !empty($_GET['filter_by_user'])) {
$filter_user = (int) $_GET['filter_by_user'];
if (empty($whereCondition)) {
$whereCondition .= " te.exe_user_id = '$filter_user'";
@@ -662,7 +660,7 @@ switch ($action) {
}
}
if (!empty($_GET['group_id_in_toolbar'])) {
if (isset($_GET['group_id_in_toolbar']) && !empty($_GET['group_id_in_toolbar'])) {
$groupIdFromToolbar = (int) $_GET['group_id_in_toolbar'];
if (!empty($groupIdFromToolbar)) {
if (empty($whereCondition)) {
@@ -681,6 +679,14 @@ switch ($action) {
$whereCondition .= " AND te.c_id = $courseId";
}
// Filtrage sur la date de fin d'exercice (exe_date)
if (!empty($search_start_date)) {
$whereCondition .= " AND te.exe_date >= '".Database::escape_string($search_start_date)." 00:00:00'";
}
if (!empty($search_end_date)) {
$whereCondition .= " AND te.exe_date <= '".Database::escape_string($search_end_date)." 23:59:59'";
}
$count = ExerciseLib::get_count_exam_results(
$exerciseId,
$whereCondition,
+17 -4
View File
@@ -251,12 +251,25 @@ switch ($action) {
$statsName = 'NumberOfUsers';
$filter = $_REQUEST['filter'];
$startDate = $_REQUEST['date_start'];
$endDate = $_REQUEST['date_end'];
$rawStartDate = isset($_REQUEST['date_start']) ? $_REQUEST['date_start'] : '';
$rawEndDate = isset($_REQUEST['date_end']) ? $_REQUEST['date_end'] : '';
$extraConditions = '';
if (!empty($startDate) && !empty($endDate)) {
$extraConditions .= " AND registration_date BETWEEN '$startDate' AND '$endDate' ";
if (!empty($rawStartDate) && !empty($rawEndDate)) {
// Validate both values against YYYY-MM-DD before embedding in SQL.
// Any other format (including SQL metacharacters) is silently ignored.
$parsedStart = DateTime::createFromFormat('Y-m-d', $rawStartDate);
$parsedEnd = DateTime::createFromFormat('Y-m-d', $rawEndDate);
if (false !== $parsedStart
&& false !== $parsedEnd
&& $parsedStart->format('Y-m-d') === $rawStartDate
&& $parsedEnd->format('Y-m-d') === $rawEndDate
) {
$startDate = Database::escape_string($rawStartDate);
$endDate = Database::escape_string($rawEndDate);
$extraConditions .= " AND registration_date BETWEEN '$startDate' AND '$endDate' ";
}
}
switch ($filter) {
+17
View File
@@ -226,6 +226,23 @@ foreach ($result as &$row) {
ini_set('log_errors', '1');
// Including configuration files
$configurationFiles = [
'mail.conf.php',
'profile.conf.php',
'course_info.conf.php',
'add_course.conf.php',
'events.conf.php',
'auth.conf.php',
];
foreach ($configurationFiles as $file) {
$file = api_get_path(CONFIGURATION_PATH).$file;
if (file_exists($file)) {
require_once $file;
}
}
/**
* Include the trad4all language file.
*/
+91 -34
View File
@@ -16,6 +16,7 @@ use Chamilo\CourseBundle\Entity\CItemProperty;
use Chamilo\UserBundle\Entity\User;
use Doctrine\ORM\Query\Expr\Join;
use Mpdf\MpdfException;
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
@@ -1417,7 +1418,7 @@ class PortfolioController
);
$urlUserString = "";
if (isset($urlUser)) {
if (!empty($urlUser)) {
$urlUserString = "user=".$urlUser;
}
@@ -2350,12 +2351,24 @@ class PortfolioController
$itemDirectory = $item->getCreationDate()->format('Y-m-d-H-i-s');
$itemFilename = sprintf('%s/items/%s/item.html', $tempPortfolioDirectory, $itemDirectory);
$itemFileContent = $this->fixImagesSourcesToHtml($itemsHtml[$i]);
$imagePaths = [];
$itemFileContent = $this->fixMediaSourcesToHtml($itemsHtml[$i], $imagePaths);
$fs->dumpFile($itemFilename, $itemFileContent);
$filenames[] = $itemFilename;
foreach ($imagePaths as $imagePath) {
$inlineFile = dirname($itemFilename).'/'.basename($imagePath);
try {
$filenames[] = $inlineFile;
$fs->copy($imagePath, $inlineFile);
} catch (FileNotFoundException $notFoundException) {
continue;
}
}
$attachments = $attachmentsRepo->findFromItem($item);
/** @var PortfolioAttachment $attachment */
@@ -2367,12 +2380,15 @@ class PortfolioController
$attachment->getFilename()
);
$fs->copy(
$attachmentsDirectory.$attachment->getPath(),
$attachmentFilename
);
$filenames[] = $attachmentFilename;
try {
$fs->copy(
$attachmentsDirectory.$attachment->getPath(),
$attachmentFilename
);
$filenames[] = $attachmentFilename;
} catch (FileNotFoundException $notFoundException) {
continue;
}
}
$tblItemsData[] = [
@@ -2397,13 +2413,25 @@ class PortfolioController
foreach ($comments as $i => $comment) {
$commentDirectory = $comment->getDate()->format('Y-m-d-H-i-s');
$commentFileContent = $this->fixImagesSourcesToHtml($commentsHtml[$i]);
$imagePaths = [];
$commentFileContent = $this->fixMediaSourcesToHtml($commentsHtml[$i], $imagePaths);
$commentFilename = sprintf('%s/comments/%s/comment.html', $tempPortfolioDirectory, $commentDirectory);
$fs->dumpFile($commentFilename, $commentFileContent);
$filenames[] = $commentFilename;
foreach ($imagePaths as $imagePath) {
$inlineFile = dirname($commentFilename).'/'.basename($imagePath);
try {
$filenames[] = $inlineFile;
$fs->copy($imagePath, $inlineFile);
} catch (FileNotFoundException $notFoundException) {
continue;
}
}
$attachments = $attachmentsRepo->findFromComment($comment);
/** @var PortfolioAttachment $attachment */
@@ -2415,12 +2443,15 @@ class PortfolioController
$attachment->getFilename()
);
$fs->copy(
$attachmentsDirectory.$attachment->getPath(),
$attachmentFilename
);
$filenames[] = $attachmentFilename;
try {
$fs->copy(
$attachmentsDirectory.$attachment->getPath(),
$attachmentFilename
);
$filenames[] = $attachmentFilename;
} catch (FileNotFoundException $notFoundException) {
continue;
}
}
$tblCommentsData[] = [
@@ -4277,44 +4308,70 @@ class PortfolioController
return $commentsHtml;
}
private function fixImagesSourcesToHtml(string $htmlContent): string
/**
* @param array $imagePaths Relative paths found in $htmlContent
*/
private function fixMediaSourcesToHtml(string $htmlContent, array &$imagePaths): string
{
$doc = new DOMDocument();
@$doc->loadHTML($htmlContent);
$elements = $doc->getElementsByTagName('img');
$tagsWithSrc = ['img', 'video', 'audio', 'source'];
/** @var array<int, \DOMElement> $elements */
$elements = [];
if (empty($elements->length)) {
foreach ($tagsWithSrc as $tag) {
foreach ($doc->getElementsByTagName($tag) as $element) {
if ($element->hasAttribute('src')) {
$elements[] = $element;
}
}
}
if (empty($elements)) {
return $htmlContent;
}
$webCoursePath = api_get_path(WEB_COURSE_PATH);
$webUploadPath = api_get_path(WEB_UPLOAD_PATH);
/** @var array<int, \DOMElement> $anchorElements */
$anchorElements = $doc->getElementsByTagName('a');
$webPath = api_get_path(WEB_PATH);
$sysPath = rtrim(api_get_path(SYS_PATH), '/');
$paths = [
'/app/upload/' => $sysPath,
'/courses/' => $sysPath.'/app',
];
/** @var \DOMElement $element */
foreach ($elements as $element) {
$src = trim($element->getAttribute('src'));
if (strpos($src, 'http') === 0) {
if (!str_starts_with($src, '/')
&& !str_starts_with($src, $webPath)
) {
continue;
}
if (strpos($src, '/app/upload/') === 0) {
$element->setAttribute(
'src',
preg_replace('/\/app/upload\//', $webUploadPath, $src, 1)
);
// to search anchors linking to files
if ($anchorElements->length > 0) {
foreach ($anchorElements as $anchorElement) {
if (!$anchorElement->hasAttribute('href')) {
continue;
}
continue;
if ($src === $anchorElement->getAttribute('href')) {
$anchorElement->setAttribute('href', basename($src));
}
}
}
if (strpos($src, '/courses/') === 0) {
$element->setAttribute(
'src',
preg_replace('/\/courses\//', $webCoursePath, $src, 1)
);
$src = str_replace($webPath, '/', $src);
continue;
foreach ($paths as $prefix => $basePath) {
if (str_starts_with($src, $prefix)) {
$imagePaths[] = $basePath.urldecode($src);
$element->setAttribute('src', basename($src));
}
}
}
+147 -60
View File
@@ -242,6 +242,39 @@ class TicketManager
return Database::store_result($result);
}
/**
* Returns the list of category IDs assigned to a user.
*
* @param int $userId
* @param int $projectId
*
* @return int[]
*/
public static function getCategoryIdsByUser($userId, $projectId = 0)
{
$tableRel = Database::get_main_table(TABLE_TICKET_CATEGORY_REL_USER);
$tableCat = Database::get_main_table(TABLE_TICKET_CATEGORY);
$userId = (int) $userId;
$projectId = (int) $projectId;
$sql = "SELECT rel.category_id
FROM $tableRel rel
INNER JOIN $tableCat cat ON (rel.category_id = cat.id)
WHERE rel.user_id = $userId";
if (!empty($projectId)) {
$sql .= " AND cat.project_id = $projectId";
}
$result = Database::query($sql);
$categories = [];
while ($row = Database::fetch_array($result)) {
$categories[] = (int) $row['category_id'];
}
return $categories;
}
/**
* @param int $categoryId
*/
@@ -322,14 +355,37 @@ class TicketManager
$course_id = (int) $course_id;
$category_id = (int) $category_id;
$project_id = (int) $project_id;
$priority = empty($priority) ? self::PRIORITY_NORMAL : (int) $priority;
if ($status === '') {
$status = self::STATUS_NEW;
// Resolve priority to numeric ID (accepts ID or code like 'NRM')
$priorityId = null;
$priorityCodeOrId = $priority;
if (empty($priorityCodeOrId)) {
$priorityCodeOrId = self::PRIORITY_NORMAL;
}
if (is_numeric($priorityCodeOrId)) {
$priorityId = (int) $priorityCodeOrId;
} else {
$priorityId = self::getPriorityIdFromCode((string) $priorityCodeOrId);
}
if (empty($priorityId)) {
// Fallback to default priority if mapping failed
$priorityId = self::getPriorityIdFromCode(self::PRIORITY_NORMAL);
}
// Resolve status to numeric ID (accepts ID or code like 'NAT')
$statusId = null;
$statusCodeOrId = $status;
if ($statusCodeOrId === '' || $statusCodeOrId === null) {
$statusCodeOrId = self::STATUS_NEW;
if ($other_area > 0) {
$status = self::STATUS_FORWARDED;
$statusCodeOrId = self::STATUS_FORWARDED;
}
}
if (is_numeric($statusCodeOrId)) {
$statusId = (int) $statusCodeOrId;
} else {
$statusId = self::getStatusIdFromCode((string) $statusCodeOrId);
}
if (!empty($category_id)) {
if (empty($assignedUserId)) {
@@ -355,9 +411,9 @@ class TicketManager
$params = [
'project_id' => $project_id,
'category_id' => $category_id,
'priority_id' => $priority,
'priority_id' => $priorityId,
'personal_email' => $personalEmail,
'status_id' => $status,
'status_id' => $statusId,
'start_date' => $now,
'sys_insert_user_id' => $currentUserId,
'sys_insert_datetime' => $now,
@@ -788,7 +844,7 @@ class TicketManager
$sql_get_message_ids = "SELECT id FROM $table_ticket_message WHERE ticket_id = $ticketId";
$sql_delete_attachments = "DELETE FROM $table_ticket_message_attachments WHERE message_id IN ($sql_get_message_ids)";
Database::query($sql_delete_attachments);
$sql_assigned_log = "DELETE FROM $table_ticket_assigned_log WHERE ticket_id = $ticketId";
Database::query($sql_assigned_log);
@@ -798,7 +854,7 @@ class TicketManager
$sql_get_category = "SELECT category_id FROM $table_support_tickets WHERE id = $ticketId";
$res = Database::query($sql_get_category);
if ($row = Database::fetch_array($res)) {
$category_id = (int)$row['category_id'];
$category_id = (int) $row['category_id'];
$table_ticket_category = Database::get_main_table('ticket_category');
$sql_update_category = "UPDATE $table_ticket_category SET total_tickets = total_tickets - 1 WHERE id = $category_id AND total_tickets > 0";
Database::query($sql_update_category);
@@ -902,7 +958,14 @@ class TicketManager
// Check if a role was set to the project
if ($userIsAllowInProject == false) {
$sql .= " AND (ticket.assigned_last_user = $userId OR ticket.sys_insert_user_id = $userId )";
$categoryList = self::getCategoryIdsByUser($userId, $projectId);
$categoryCondition = '';
if (!empty($categoryList)) {
$categoryIds = implode(',', array_map('intval', $categoryList));
$categoryCondition = " OR ticket.category_id IN ($categoryIds)";
}
$sql .= " AND (ticket.assigned_last_user = $userId OR ticket.sys_insert_user_id = $userId".$categoryCondition.")";
}
// Search simple
@@ -948,11 +1011,11 @@ class TicketManager
$keyword_range = !empty($keyword_start_date_start) && !empty($keyword_start_date_end);
if ($keyword_range == false && $keyword_start_date_start != '') {
$sql .= " AND DATE_FORMAT(ticket.start_date,'%d/%m/%Y') >= '$keyword_start_date_start' ";
$sql .= " AND ticket.start_date >= '$keyword_start_date_start' ";
}
if ($keyword_range && $keyword_start_date_start != '' && $keyword_start_date_end != '') {
$sql .= " AND DATE_FORMAT(ticket.start_date,'%d/%m/%Y') >= '$keyword_start_date_start'
AND DATE_FORMAT(ticket.start_date,'%d/%m/%Y') <= '$keyword_start_date_end'";
$sql .= " AND ticket.start_date >= '$keyword_start_date_start'
AND ticket.start_date <= '$keyword_start_date_end'";
}
if ($keyword_course != '') {
@@ -1047,7 +1110,7 @@ class TicketManager
if ($isAdmin) {
$project_id = isset($row['project_id']) ? $row['project_id'] : (isset($_GET['project_id']) ? $_GET['project_id'] : 0);
$delete_link = '<a href="tickets.php?action=delete&ticket_id='.$row['ticket_id'].'&project_id='.$project_id.'" onclick="return confirm(\''.htmlentities(get_lang('AreYouSureYouWantToDeleteThisTicket')).'\')">'
. Display::return_icon('delete.png', get_lang('Delete')) .
.Display::return_icon('delete.png', get_lang('Delete')).
'</a>';
$ticket[] = $delete_link;
}
@@ -1072,51 +1135,54 @@ class TicketManager
if (empty($userInfo)) {
return 0;
}
$userId = $userInfo['id'];
$userId = (int) $userInfo['id'];
if (!isset($_GET['project_id'])) {
return 0;
}
$sql = "SELECT COUNT(ticket.id) AS total
$sql = "SELECT COUNT(DISTINCT ticket.id) AS total
FROM $table_support_tickets ticket
INNER JOIN $table_support_category cat
ON (cat.id = ticket.category_id)
ON (cat.id = ticket.category_id)
INNER JOIN $table_support_priority priority
ON (ticket.priority_id = priority.id)
ON (ticket.priority_id = priority.id)
INNER JOIN $table_support_status status
ON (ticket.status_id = status.id)
WHERE 1 = 1";
ON (ticket.status_id = status.id)
WHERE 1 = 1";
$projectId = (int) $_GET['project_id'];
$allowRoleList = self::getAllowedRolesFromProject($projectId);
$userIsAllowInProject = self::userIsAllowInProject($userInfo, $projectId);
// Check if a role was set to the project
if (!empty($allowRoleList) && is_array($allowRoleList)) {
if (!in_array($userInfo['status'], $allowRoleList)) {
$sql .= " AND (ticket.assigned_last_user = $userId OR ticket.sys_insert_user_id = $userId )";
}
} else {
if (!api_is_platform_admin()) {
$sql .= " AND (ticket.assigned_last_user = $userId OR ticket.sys_insert_user_id = $userId )";
// Apply same permission constraints as getTicketsByCurrentUser
if ($userIsAllowInProject == false) {
$categoryList = self::getCategoryIdsByUser($userId, $projectId);
$categoryCondition = '';
if (!empty($categoryList)) {
$categoryIds = implode(',', array_map('intval', $categoryList));
$categoryCondition = " OR ticket.category_id IN ($categoryIds)";
}
$sql .= " AND (ticket.assigned_last_user = $userId OR ticket.sys_insert_user_id = $userId".$categoryCondition.")";
}
// Search simple
if (isset($_GET['submit_simple'])) {
if ($_GET['keyword'] != '') {
$keyword = Database::escape_string(trim($_GET['keyword']));
$sql .= " AND (
ticket.code LIKE '%$keyword%' OR
ticket.subject LIKE '%$keyword%' OR
ticket.message LIKE '%$keyword%' OR
ticket.keyword LIKE '%$keyword%' OR
ticket.personal_email LIKE '%$keyword%' OR
ticket.source LIKE '%$keyword%'
)";
}
// Simple search (align with getTicketsByCurrentUser)
if (isset($_GET['submit_simple']) && $_GET['keyword'] !== '') {
$keyword = Database::escape_string(trim($_GET['keyword']));
$sql .= " AND (
ticket.id LIKE '%$keyword%' OR
ticket.code LIKE '%$keyword%' OR
ticket.subject LIKE '%$keyword%' OR
ticket.message LIKE '%$keyword%' OR
ticket.keyword LIKE '%$keyword%' OR
ticket.source LIKE '%$keyword%' OR
cat.name LIKE '%$keyword%' OR
status.name LIKE '%$keyword%' OR
priority.name LIKE '%$keyword%' OR
ticket.personal_email LIKE '%$keyword%'
)";
}
// Exact-match filters
$keywords = [
'project_id' => 'ticket.project_id',
'keyword_category' => 'ticket.category_id',
@@ -1128,42 +1194,42 @@ class TicketManager
];
foreach ($keywords as $keyword => $sqlLabel) {
if (!empty($_GET[$keyword])) {
if (isset($_GET[$keyword])) {
$data = Database::escape_string(trim($_GET[$keyword]));
$sql .= " AND $sqlLabel = '$data' ";
if ($data !== '') {
$sql .= " AND $sqlLabel = '$data' ";
}
}
}
// Search advanced
// Advanced search: date range and course
$keyword_start_date_start = isset($_GET['keyword_start_date_start']) ? Database::escape_string(trim($_GET['keyword_start_date_start'])) : '';
$keyword_start_date_end = isset($_GET['keyword_start_date_end']) ? Database::escape_string(trim($_GET['keyword_start_date_end'])) : '';
$keyword_range = isset($_GET['keyword_dates']) ? Database::escape_string(trim($_GET['keyword_dates'])) : '';
$keyword_course = isset($_GET['keyword_course']) ? Database::escape_string(trim($_GET['keyword_course'])) : '';
$keyword_range = !empty($keyword_start_date_start) && !empty($keyword_start_date_end);
if ($keyword_range == false && $keyword_start_date_start != '') {
$sql .= " AND DATE_FORMAT( ticket.start_date,'%d/%m/%Y') = '$keyword_start_date_start' ";
if (!$keyword_range && $keyword_start_date_start !== '') {
$sql .= " AND ticket.start_date >= '$keyword_start_date_start' ";
}
if ($keyword_range && $keyword_start_date_start != '' && $keyword_start_date_end != '') {
$sql .= " AND DATE_FORMAT( ticket.start_date,'%d/%m/%Y') >= '$keyword_start_date_start'
AND DATE_FORMAT( ticket.start_date,'%d/%m/%Y') <= '$keyword_start_date_end'";
if ($keyword_range) {
$sql .= " AND ticket.start_date >= '$keyword_start_date_start' AND ticket.start_date <= '$keyword_start_date_end'";
}
if ($keyword_course != '') {
if ($keyword_course !== '') {
$course_table = Database::get_main_table(TABLE_MAIN_COURSE);
$sql .= " AND ticket.course_id IN (
SELECT id
FROM $course_table
WHERE (
title LIKE '%$keyword_course%' OR
code LIKE '%$keyword_course%' OR
visual_code LIKE '%$keyword_course%'
)
) ";
SELECT id FROM $course_table
WHERE (
title LIKE '%$keyword_course%' OR
code LIKE '%$keyword_course%' OR
visual_code LIKE '%$keyword_course%'
)
)";
}
$res = Database::query($sql);
$obj = Database::fetch_object($res);
return (int) $obj->total;
return $obj ? (int) $obj->total : 0;
}
/**
@@ -1964,6 +2030,27 @@ class TicketManager
return 0;
}
/**
* Returns the numeric priority ID from its code (e.g. 'NRM', 'HGH').
*
* @param string $code
*
* @return int
*/
public static function getPriorityIdFromCode($code)
{
$item = Database::getManager()
->getRepository('ChamiloTicketBundle:Priority')
->findOneBy(['code' => $code])
;
if ($item) {
return $item->getId();
}
return 0;
}
/**
* @return array
*/
+5 -5
View File
@@ -86,11 +86,11 @@ class UnserializeApi
[
learnpath::class,
learnpathItem::class,
aicc::class,
aiccBlock::class,
aiccItem::class,
aiccObjective::class,
aiccResource::class,
//aicc::class,
//aiccBlock::class,
//aiccItem::class,
//aiccObjective::class,
//aiccResource::class,
scorm::class,
scormItem::class,
scormMetadata::class,
+11 -11
View File
@@ -1224,15 +1224,15 @@ class AddCourse
$code = $params['code'];
$visual_code = $params['visual_code'];
$directory = $params['directory'];
$tutor_name = isset($params['tutor_name']) ? $params['tutor_name'] : null;
$category_code = isset($params['course_category']) ? $params['course_category'] : '';
$course_language = isset($params['course_language']) && !empty($params['course_language']) ? $params['course_language'] : api_get_setting(
'platformLanguage'
);
$tutor_name = $params['tutor_name'] ?? null;
$category_code = $params['course_category'] ?? '';
$course_language = !empty($params['course_language'])
? $params['course_language']
: api_get_setting('platformLanguage');
$user_id = empty($params['user_id']) ? api_get_user_id() : (int) $params['user_id'];
$department_name = isset($params['department_name']) ? $params['department_name'] : null;
$department_url = isset($params['department_url']) ? $params['department_url'] : null;
$disk_quota = isset($params['disk_quota']) ? $params['disk_quota'] : null;
$department_name = $params['department_name'] ?? null;
$department_url = $params['department_url'] ?? null;
$disk_quota = $params['disk_quota'] ?? null;
if (!isset($params['visibility'])) {
$default_course_visibility = api_get_setting(
@@ -1253,9 +1253,9 @@ class AddCourse
$subscribe = $visibility == COURSE_VISIBILITY_OPEN_PLATFORM ? 1 : 0;
}
$unsubscribe = isset($params['unsubscribe']) ? (int) $params['unsubscribe'] : 0;
$expiration_date = isset($params['expiration_date']) ? $params['expiration_date'] : null;
$teachers = isset($params['teachers']) ? $params['teachers'] : null;
$status = isset($params['status']) ? $params['status'] : null;
$expiration_date = $params['expiration_date'] ?? null;
$teachers = $params['teachers'] ?? null;
$status = $params['status'] ?? null;
$TABLECOURSE = Database::get_main_table(TABLE_MAIN_COURSE);
$TABLECOURSUSER = Database::get_main_table(TABLE_MAIN_COURSE_USER);
+433 -12
View File
@@ -4338,6 +4338,11 @@ function api_get_item_visibility(
$groupCondition = " AND to_group_id = '$group_id' ";
}
$lpVisibilityCondition = '';
if ($tool === 'learnpath') {
$lpVisibilityCondition = " AND lastedit_type != 'LearnpathSubscription' ";
}
$sql = "SELECT visibility
FROM $TABLE_ITEMPROPERTY
WHERE
@@ -4345,7 +4350,7 @@ function api_get_item_visibility(
tool = '$tool' AND
ref = $id AND
(session_id = $session OR session_id = 0 OR session_id IS NULL)
$userCondition $typeCondition $groupCondition
$userCondition $typeCondition $groupCondition $lpVisibilityCondition
ORDER BY session_id DESC, lastedit_date DESC
LIMIT 1";
@@ -4991,7 +4996,7 @@ function api_get_item_property_info($course_id, $tool, $ref, $session_id = 0, $g
*
* @return array with all fields from c_item_property, empty array if not found or false if course could not be found
*/
function api_get_last_item_property_info(int $courseId, string $tool, int $ref, int $sessionId = null, int $groupId = null): array
function api_get_last_item_property_info(int $courseId, string $tool, int $ref, ?int $sessionId = null, ?int $groupId = null): array
{
$tool = Database::escape_string($tool);
// Definition of tables.
@@ -7163,6 +7168,22 @@ function api_is_xml_http_request()
*/
function api_getimagesize($path)
{
$path = str_replace(
[
api_get_path(WEB_COURSE_PATH),
api_get_path(WEB_UPLOAD_PATH),
api_get_path(WEB_CODE_PATH),
api_get_path(WEB_PATH),
],
[
api_get_path(SYS_COURSE_PATH),
api_get_path(SYS_UPLOAD_PATH),
api_get_path(SYS_CODE_PATH),
api_get_path(SYS_PATH),
],
$path
);
$image = new Image($path);
return $image->get_image_size();
@@ -8955,6 +8976,10 @@ function api_can_login_as($loginAsUserId, $userId = null)
$userInfo = api_get_user_info($loginAsUserId);
$isDrh = function () use ($loginAsUserId) {
if (api_is_drh()) {
if (true === api_get_configuration_value('disallow_hrm_login_as')) {
return false;
}
if (api_drh_can_access_all_session_content()) {
$users = SessionManager::getAllUsersFromCoursesFromAllSessionFromStatus(
'drh_all',
@@ -8981,14 +9006,26 @@ function api_can_login_as($loginAsUserId, $userId = null)
return false;
};
$loginAsStatusForSessionAdmins = [STUDENT];
$allowSessionAdmin = function () use ($userInfo) {
if (!api_is_session_admin()) {
return false;
}
if (api_get_configuration_value('allow_session_admin_login_as_teacher')) {
$loginAsStatusForSessionAdmins[] = COURSEMANAGER;
}
if (true === api_get_configuration_value('disallow_session_admin_login_as')) {
return false;
}
$loginAsStatusForSessionAdmins = [STUDENT];
if (api_get_configuration_value('allow_session_admin_login_as_teacher')) {
$loginAsStatusForSessionAdmins[] = COURSEMANAGER;
}
return in_array($userInfo['status'], $loginAsStatusForSessionAdmins);
};
return api_is_platform_admin() ||
(api_is_session_admin() && in_array($userInfo['status'], $loginAsStatusForSessionAdmins)) ||
$allowSessionAdmin() ||
$isDrh();
}
@@ -10234,11 +10271,11 @@ function api_unserialize_content($type, $serialized, $ignoreErrors = false)
$allowedClasses = [
learnpath::class,
learnpathItem::class,
aicc::class,
aiccBlock::class,
aiccItem::class,
aiccObjective::class,
aiccResource::class,
//aicc::class,
//aiccBlock::class,
//aiccItem::class,
//aiccObjective::class,
//aiccResource::class,
scorm::class,
scormItem::class,
scormMetadata::class,
@@ -10699,3 +10736,387 @@ function api_encrypt_hash($data, $secret)
return base64_encode($iv).base64_encode($encrypted.$tag);
}
/**
* Replace a specific term by another in all course-related text elements in the database.
* Does not rename directories or replace content of files on disk. Check tests/scripts/replace_course_code.php if
* you are looking for this.
* The replacement can replace bits in larger strings, requiring the search string to be very specific to avoid
* excess replacements.
*
* @return array The number of changes executed in each table
*/
function api_replace_terms_in_content(string $search, string $replace): array
{
$replacements = [
Database::get_course_table(TABLE_QUIZ_TEST) => [
'iid' => ['title', 'description', 'sound'],
],
Database::get_course_table(TABLE_QUIZ_QUESTION) => [
'iid' => ['question', 'description'],
],
Database::get_course_table(TABLE_QUIZ_ANSWER) => [
'iid' => ['answer', 'comment'],
],
Database::get_course_table(TABLE_ANNOUNCEMENT) => [
'iid' => ['title', 'content'],
],
Database::get_course_table(TABLE_ANNOUNCEMENT_ATTACHMENT) => [
'iid' => ['path', 'comment'],
],
Database::get_course_table(TABLE_ATTENDANCE) => [
'iid' => ['name', 'description', 'attendance_qualify_title'],
],
Database::get_course_table(TABLE_BLOGS) => [
'iid' => ['blog_name', 'blog_subtitle'],
],
Database::get_course_table(TABLE_BLOGS_ATTACHMENT) => [
'iid' => ['path', 'comment'],
],
Database::get_course_table(TABLE_BLOGS_COMMENTS) => [
'iid' => ['title', 'comment'],
],
Database::get_course_table(TABLE_BLOGS_POSTS) => [
'iid' => ['title', 'full_text'],
],
Database::get_course_table(TABLE_BLOGS_TASKS) => [
'iid' => ['title', 'description'],
],
Database::get_course_table(TABLE_AGENDA) => [
'iid' => ['title', 'content', 'comment'],
],
Database::get_course_table(TABLE_AGENDA_ATTACHMENT) => [
'iid' => ['path', 'comment', 'filename'],
],
Database::get_course_table(TABLE_COURSE_DESCRIPTION) => [
'iid' => ['title', 'content'],
],
Database::get_course_table(TABLE_DOCUMENT) => [
'iid' => ['path', 'comment'],
],
Database::get_course_table(TABLE_DROPBOX_FEEDBACK) => [
'iid' => ['feedback'],
],
Database::get_course_table(TABLE_DROPBOX_FILE) => [
'iid' => ['title', 'description'],
],
Database::get_course_table(TABLE_DROPBOX_POST) => [
'iid' => ['feedback'],
],
Database::get_course_table(TABLE_FORUM_ATTACHMENT) => [
'iid' => ['path', 'comment', 'filename'],
],
Database::get_course_table(TABLE_FORUM_CATEGORY) => [
'iid' => ['cat_title', 'cat_comment'],
],
Database::get_course_table(TABLE_FORUM) => [
'iid' => ['forum_title', 'forum_comment', 'forum_image'],
],
Database::get_course_table(TABLE_FORUM_POST) => [
'iid' => ['post_title', 'post_text', 'poster_name'],
],
Database::get_course_table(TABLE_FORUM_THREAD) => [
'iid' => ['thread_title', 'thread_poster_name', 'thread_title_qualify'],
],
Database::get_course_table(TABLE_GLOSSARY) => [
'iid' => ['name', 'description'],
],
Database::get_course_table(TABLE_GROUP_CATEGORY) => [
'iid' => ['title', 'description'],
],
Database::get_course_table(TABLE_GROUP) => [
'iid' => ['name', 'description', 'secret_directory'],
],
Database::get_course_table(TABLE_LINK) => [
'iid' => ['description'],
],
Database::get_course_table(TABLE_LINK_CATEGORY) => [
'iid' => ['category_title', 'description'],
],
Database::get_course_table(TABLE_LP_MAIN) => [
'iid' => ['name', 'ref', 'description', 'path', 'content_license', 'preview_image', 'theme'],
],
Database::get_course_table(TABLE_LP_CATEGORY) => [
'iid' => ['name'],
],
Database::get_course_table(TABLE_LP_ITEM) => [
'iid' => ['prerequisite', 'description', 'title', 'parameters', 'launch_data', 'terms'],
],
Database::get_course_table(TABLE_LP_ITEM_VIEW) => [
'iid' => ['suspend_data', 'lesson_location'],
],
Database::get_course_table(TABLE_NOTEBOOK) => [
'iid' => ['title', 'description'],
],
Database::get_course_table(TABLE_ONLINE_LINK) => [
'iid' => ['name'],
],
Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY) => [
'iid' => ['title', 'description'],
],
Database::get_course_table(TABLE_ROLE) => [
'iid' => ['role_name', 'role_comment'],
],
Database::get_course_table(TABLE_STUDENT_PUBLICATION) => [
'iid' => ['title', 'title_correction', 'description'],
],
Database::get_course_table(TABLE_STUDENT_PUBLICATION_ASSIGNMENT_COMMENT) => [
'iid' => ['comment', 'file'],
],
Database::get_course_table(TABLE_SURVEY) => [
'iid' => ['title', 'subtitle', 'surveythanks', 'invite_mail', 'reminder_mail', 'mail_subject', 'access_condition', 'form_fields'],
],
Database::get_course_table(TABLE_SURVEY_QUESTION_GROUP) => [
'iid' => ['name', 'description'],
],
Database::get_course_table(TABLE_SURVEY_QUESTION) => [
'iid' => ['survey_question', 'survey_question_comment'],
],
Database::get_course_table(TABLE_SURVEY_QUESTION_OPTION) => [
'iid' => ['option_text'],
],
Database::get_course_table(TABLE_THEMATIC) => [
'iid' => ['content', 'title'],
],
Database::get_course_table(TABLE_THEMATIC_ADVANCE) => [
'iid' => ['content'],
],
Database::get_course_table(TABLE_THEMATIC_PLAN) => [
'iid' => ['description'],
],
Database::get_course_table(TABLE_TOOL_LIST) => [
'iid' => ['description'],
],
Database::get_course_table(TABLE_TOOL_INTRO) => [
'iid' => ['intro_text'],
],
Database::get_course_table(TABLE_USER_INFO_DEF) => [
'iid' => ['comment'],
],
Database::get_course_table(TABLE_WIKI) => [
'iid' => ['title', 'content', 'comment', 'progress', 'linksto'],
],
Database::get_course_table(TABLE_WIKI_CONF) => [
'iid' => ['feedback1', 'feedback2', 'feedback3'],
],
Database::get_course_table(TABLE_WIKI_DISCUSS) => [
'iid' => ['comment'],
],
Database::get_main_table(TABLE_CAREER) => [
'id' => ['name', 'description'],
],
Database::get_main_table(TABLE_MAIN_CHAT) => [
'id' => ['message'],
],
Database::get_main_table(TABLE_MAIN_CLASS) => [
'id' => ['name'],
],
Database::get_main_table(TABLE_MAIN_COURSE_REQUEST) => [
'id' => ['description', 'title', 'objetives', 'target_audience'],
],
'course_type' => [
'id' => ['description'],
],
Database::get_main_table(TABLE_EVENT_EMAIL_TEMPLATE) => [
'id' => ['message', 'subject', 'event_type_name'],
],
Database::get_main_table(TABLE_GRADE_MODEL) => [
'id' => ['name', 'description'],
],
Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY) => [
'id' => ['name', 'description'],
],
Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE) => [
'id' => ['path_certificate'],
],
Database::get_main_table(TABLE_MAIN_GRADEBOOK_EVALUATION) => [
'id' => ['description', 'name'],
],
Database::get_main_table(TABLE_MAIN_GRADEBOOK_LINKEVAL_LOG) => [
'id' => ['name', 'description'],
],
Database::get_main_table(TABLE_MAIN_LEGAL) => [
'id' => ['content', 'changes'],
],
Database::get_main_table(TABLE_MESSAGE) => [
'id' => ['content'],
],
Database::get_main_table(TABLE_MESSAGE_ATTACHMENT) => [
'id' => ['path', 'comment', 'filename'],
],
Database::get_main_table(TABLE_NOTIFICATION) => [
'id' => ['content'],
],
Database::get_main_table(TABLE_PERSONAL_AGENDA) => [
'id' => ['title', 'text'],
],
Database::get_main_table(TABLE_PROMOTION) => [
'id' => ['description'],
],
'room' => [
'id' => ['description'],
],
'sequence_condition' => [
'id' => ['description'],
],
'sequence_method' => [
'id' => ['description', 'formula'],
],
'sequence_rule' => [
'id' => ['description'],
],
'sequence_type_entity' => [
'id' => ['description'],
],
'sequence_variable' => [
'id' => ['description'],
],
Database::get_main_table(TABLE_MAIN_SESSION) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_MAIN_SHARED_SURVEY) => [
'survey_id' => ['subtitle', 'surveythanks', 'intro'],
],
Database::get_main_table(TABLE_MAIN_SHARED_SURVEY_QUESTION) => [
'question_id' => ['survey_question', 'survey_question_comment'],
],
Database::get_main_table(TABLE_MAIN_SHARED_SURVEY_QUESTION_OPTION) => [
'question_option_id' => ['option_text'],
],
Database::get_main_table(TABLE_MAIN_SKILL) => [
'id' => ['name', 'description', 'criteria'],
],
Database::get_main_table(TABLE_MAIN_SKILL_PROFILE) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_MAIN_SKILL_REL_USER) => [
'id' => ['argumentation'],
],
'skill_rel_user_comment' => [
'id' => ['feedback_text'],
],
Database::get_main_table(TABLE_MAIN_SYSTEM_ANNOUNCEMENTS) => [
'id' => ['content'],
],
Database::get_main_table(TABLE_MAIN_SYSTEM_CALENDAR) => [
'id' => ['content'],
],
Database::get_main_table(TABLE_MAIN_SYSTEM_TEMPLATE) => [
'id' => ['comment', 'content'],
],
Database::get_main_table(TABLE_MAIN_TEMPLATES) => [
'id' => ['description', 'image'],
],
Database::get_main_table(TABLE_TICKET_CATEGORY) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_TICKET_MESSAGE) => [
'id' => ['message'],
],
Database::get_main_table(TABLE_TICKET_MESSAGE_ATTACHMENTS) => [
'id' => ['filename', 'path'],
],
Database::get_main_table(TABLE_TICKET_PRIORITY) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_TICKET_PROJECT) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_TICKET_STATUS) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_TICKET_TICKET) => [
'id' => ['message'],
],
Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT) => [
'id' => ['answer', 'teacher_comment', 'filename'],
],
Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING) => [
'id' => ['teacher_comment'],
],
Database::get_main_table(TABLE_STATISTIC_TRACK_E_DEFAULT) => [
'default_id' => ['default_value'],
],
Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES) => [
'exe_id' => ['data_tracking', 'questions_to_check'],
],
Database::get_main_table(TABLE_STATISTIC_TRACK_E_ITEM_PROPERTY) => [
'id' => ['content'],
],
'track_e_open' => [
'open_id' => ['open_remote_host', 'open_agent', 'open_referer'],
],
Database::get_main_table(TABLE_TRACK_STORED_VALUES) => [
'id' => ['sv_value'],
],
Database::get_main_table(TABLE_TRACK_STORED_VALUES_STACK) => [
'id' => ['sv_value'],
],
Database::get_main_table(TABLE_MAIN_USER_API_KEY) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_USERGROUP) => [
'id' => ['name', 'description', 'picture', 'url'],
],
Database::get_main_table(TABLE_MAIN_BLOCK) => [
'id' => ['name', 'description', 'path'],
],
];
if (api_get_configuration_value('attendance_allow_comments')) {
$replacements['c_attendance_result_comment'] = [
'iid' => ['comment'],
];
}
if (api_get_configuration_value('exercise_text_when_finished_failure')) {
$replacements[Database::get_course_table(TABLE_QUIZ_TEST)]['iid'][] = 'text_when_finished_failure';
}
$changes = array_map(
fn ($table) => 0,
$replacements
);
foreach ($replacements as $table => $replacement) {
foreach ($replacement as $idColumn => $columns) {
$keys = array_map(fn ($column) => "$column LIKE %?%", $columns);
$values = array_fill(0, count($columns), $search);
$result = Database::select(
[$idColumn, ...$columns],
$table,
[
'where' => [
implode(' OR ', $keys) => $values,
],
'order' => "$idColumn ASC",
]
);
foreach ($result as $row) {
$attributes = array_combine(
$columns,
array_map(
fn ($column) => preg_replace('#'.$search.'#', $replace, $row[$column]),
$columns
)
);
try {
Database::update(
$table,
$attributes,
["$idColumn = ?" => $row[$idColumn]]
);
} catch (Exception $e) {
Database::handleError($e);
}
$changes[$table]++;
}
}
}
return $changes;
}
+14 -1
View File
@@ -776,6 +776,10 @@ class Attendance
);
$value['photo'] = $photo;
$addOfficialCode = api_get_configuration_value('attendance_add_official_code');
if ($addOfficialCode) {
$value['official_code'] = $user_data['official_code'];
}
$value['firstname'] = $user_data['firstname'];
$value['lastname'] = $user_data['lastname'];
$value['username'] = $user_data['username'];
@@ -1321,6 +1325,7 @@ class Attendance
if (Database::num_rows($res) > 0) {
while ($row = Database::fetch_array($res)) {
$row['duration'] = Attendance::getAttendanceCalendarExtraFieldValue('duration', $row['calendar_id']);
$row['date_time'] = api_get_local_time($row['date_time']);
$row['date_time'] = api_convert_and_format_date($row['date_time'], null, date_default_timezone_get());
$data[$user_id][] = $row;
}
@@ -2722,7 +2727,12 @@ class Attendance
// Get data table
$dataTable = [];
$headTable = ['#', get_lang('Name')];
$addOfficialCode = api_get_configuration_value('attendance_add_official_code');
if ($addOfficialCode) {
$headTable = ['#', get_lang('OfficialCode'), get_lang('Name')];
} else {
$headTable = ['#', get_lang('Name')];
}
foreach ($calendar as $classDay) {
$labelDuration = !empty($classDay['duration']) ? get_lang('Duration').' : '.$classDay['duration'] : '';
$headTable[] =
@@ -2739,6 +2749,9 @@ class Attendance
$cols = 1;
$result = [];
$result['count'] = $count;
if ($addOfficialCode) {
$result['official_code'] = $user['official_code'];
}
$result['full_name'] = api_get_person_name($user['firstname'], $user['lastname']);
foreach ($calendar as $classDay) {
$commentInfo = $this->getComment($user['user_id'], $classDay['id']);
+2 -2
View File
@@ -234,8 +234,8 @@ class Auth
$result = false;
$table = Database::get_main_table(TABLE_USER_COURSE_CATEGORY);
$sql = "UPDATE $table
SET title='".api_htmlentities($title, ENT_QUOTES, api_get_system_encoding())."'
WHERE id='".$category_id."'";
SET title = '".api_htmlentities($title, ENT_QUOTES, api_get_system_encoding())."'
WHERE id = ".$category_id." AND user_id = ".api_get_user_id();
$resultQuery = Database::query($sql);
if (Database::affected_rows($resultQuery)) {
$result = true;
+19 -4
View File
@@ -3030,16 +3030,31 @@ class Blog
$visibility_icon = ($info_log[2] == 0) ? 'invisible' : 'visible';
$visibility_info = ($info_log[2] == 0) ? 'Visible' : 'Invisible';
$my_image = '<a href="'.api_get_self().'?action=visibility&blog_id='.$info_log[3].'">';
$secToken = Security::get_existing_token('blog');
$my_image = '<a href="'.api_get_self().'?'
.http_build_query([
'action' => 'visibility',
'blog_id' => $info_log[3],
'blog_sec_token' => $secToken,
]).'">';
$my_image .= Display::return_icon($visibility_icon.'.png', get_lang($visibility_info));
$my_image .= "</a>";
$my_image .= '<a href="'.api_get_self().'?action=edit&blog_id='.$info_log[3].'">';
$my_image .= '<a href="'.api_get_self().'?'
.http_build_query([
'action' => 'edit',
'blog_id' => $info_log[3],
]).'">';
$my_image .= Display::return_icon('edit.png', get_lang('EditBlog'));
$my_image .= "</a>";
$my_image .= '<a href="'.api_get_self().'?action=delete&blog_id='.$info_log[3].'" ';
$my_image .= 'onclick="javascript:if(!confirm(\''.addslashes(
$my_image .= '<a href="'.api_get_self().'?'
.http_build_query([
'action' => 'delete',
'blog_id' => $info_log[3],
'blog_sec_token' => $secToken,
]).'" onclick="javascript:if(!confirm(\''.addslashes(
api_htmlentities(get_lang("ConfirmYourChoice"), ENT_QUOTES, $charset)
).'\')) return false;" >';
$my_image .= Display::return_icon('delete.png', get_lang('DeleteBlog'));
+63 -14
View File
@@ -8,6 +8,7 @@ use Chamilo\CoreBundle\Entity\Repository\SequenceResourceRepository;
use Chamilo\CoreBundle\Entity\SequenceResource;
use Chamilo\CourseBundle\Component\CourseCopy\CourseBuilder;
use Chamilo\CourseBundle\Component\CourseCopy\CourseRestorer;
use Chamilo\CourseBundle\Entity\CCourseDescription;
use ChamiloSession as Session;
use Doctrine\Common\Collections\Criteria;
@@ -93,7 +94,7 @@ class CourseManager
// Create the course keys
$keys = AddCourse::define_course_keys($params['wanted_code']);
$params['exemplary_content'] = isset($params['exemplary_content']) ? $params['exemplary_content'] : false;
$params['exemplary_content'] = $params['exemplary_content'] ?? false;
if (count($keys)) {
$params['code'] = $keys['currentCourseCode'];
@@ -2436,7 +2437,7 @@ class CourseManager
return [];
}
$session_id != 0 ? $session_condition = ' WHERE g.session_id IN(1,'.intval($session_id).')' : $session_condition = ' WHERE g.session_id = 0';
$session_id != 0 ? $session_condition = ' WHERE g.session_id = '.intval($session_id) : $session_condition = ' WHERE g.session_id = 0';
if ($in_get_empty_group == 0) {
// get only groups that are not empty
$sql = "SELECT DISTINCT g.id, g.iid, g.name
@@ -3442,24 +3443,24 @@ class CourseManager
/**
* Lists details of the course description.
*
* @param array The course description
* @param string The encoding
* @param bool If true is displayed if false is hidden
* @param array<int, CCourseDescription> $descriptions The course description
* @param string $charset The encoding
* @param bool $action_show If true is displayed if false is hidden
*
* @return string The course description in html
*/
public static function get_details_course_description_html(
$descriptions,
$charset,
$action_show = true
) {
array $descriptions,
string $charset,
bool $action_show = true
): ?string {
$data = null;
if (isset($descriptions) && count($descriptions) > 0) {
if (count($descriptions) > 0) {
foreach ($descriptions as $description) {
$data .= '<div class="sectiontitle">';
if (api_is_allowed_to_edit() && $action_show) {
//delete
$data .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&action=delete&description_id='.$description->id.'" onclick="javascript:if(!confirm(\''.addslashes(api_htmlentities(
$data .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&action=delete&description_id='.$description->getIid().'" onclick="javascript:if(!confirm(\''.addslashes(api_htmlentities(
get_lang('ConfirmYourChoice'),
ENT_QUOTES,
$charset
@@ -3471,7 +3472,7 @@ class CourseManager
);
$data .= '</a> ';
//edit
$data .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&description_id='.$description->id.'">';
$data .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&description_id='.$description->getIid().'">';
$data .= Display::return_icon(
'edit.png',
get_lang('Edit'),
@@ -3480,10 +3481,10 @@ class CourseManager
);
$data .= '</a> ';
}
$data .= $description->title;
$data .= Security::remove_XSS($description->getTitle());
$data .= '</div>';
$data .= '<div class="sectioncomment">';
$data .= Security::remove_XSS($description->content);
$data .= Security::remove_XSS($description->getContent());
$data .= '</div>';
}
} else {
@@ -7435,6 +7436,54 @@ class CourseManager
return $logo;
}
public static function searchCourse(string $searchTerm, ?int $sessionId = null): array
{
if (!empty($sessionId)) {
//if session is defined, lets find only courses of this session
return SessionManager::get_course_list_by_session_id(
$sessionId,
$searchTerm
);
}
//if session is not defined lets search all courses STARTING with $searchTerm
//TODO change this function to search not only courses STARTING with $searchTerm
if (api_is_platform_admin()) {
return CourseManager::get_courses_list(
0,
0,
'title',
'ASC',
-1,
$searchTerm,
null,
true
);
}
if (api_is_teacher()) {
$courseList = CourseManager::get_course_list_of_user_as_course_admin(api_get_user_id(), $searchTerm);
$category = api_get_configuration_value('course_category_code_to_use_as_model');
if (!empty($category)) {
$alreadyAdded = [];
if (!empty($courseList)) {
$alreadyAdded = array_column($courseList, 'id');
}
$coursesInCategory = CourseCategory::getCoursesInCategory($category, $searchTerm);
foreach ($coursesInCategory as $course) {
if (!in_array($course['id'], $alreadyAdded)) {
$courseList[] = $course;
}
}
}
return $courseList;
}
return [];
}
/**
* Check if a specific access-url-related setting is a problem or not.
*
+52 -8
View File
@@ -3,6 +3,7 @@
/* For licensing terms, see /license.txt */
use ChamiloSession as Session;
use enshrined\svgSanitize\Sanitizer;
/**
* Class DocumentManager
@@ -486,6 +487,14 @@ class DocumentManager
}
echo $content;
} else {
if ('image/svg+xml' === $contentType) {
$svgContent = file_get_contents($full_file_name);
echo (new Sanitizer())->sanitize($svgContent);
return true;
}
if (isset($enableMathJaxScript) && $enableMathJaxScript === true) {
$content = file_get_contents($full_file_name);
$content = self::includeMathJaxScript($content);
@@ -3207,27 +3216,27 @@ class DocumentManager
fclose($handle);
break;
case 'application/pdf':
exec("pdftotext $doc_path -", $output, $ret_val);
exec("pdftotext ".escapeshellarg($doc_path)." -", $output, $ret_val);
break;
case 'application/postscript':
$temp_file = tempnam(sys_get_temp_dir(), 'chamilo');
exec("ps2pdf $doc_path $temp_file", $output, $ret_val);
exec("ps2pdf ".escapeshellarg($doc_path)." ".escapeshellarg($temp_file), $output, $ret_val);
if ($ret_val !== 0) { // shell fail, probably 127 (command not found)
return false;
}
exec("pdftotext $temp_file -", $output, $ret_val);
exec("pdftotext ".escapeshellarg($temp_file)." -", $output, $ret_val);
unlink($temp_file);
break;
case 'application/msword':
exec("catdoc $doc_path", $output, $ret_val);
exec("catdoc ".escapeshellarg($doc_path), $output, $ret_val);
break;
case 'text/html':
exec("html2text $doc_path", $output, $ret_val);
exec("html2text ".escapeshellarg($doc_path), $output, $ret_val);
break;
case 'text/rtf':
// Note: correct handling of code pages in unrtf
// on debian lenny unrtf v0.19.2 can not, but unrtf v0.20.5 can
exec("unrtf --text $doc_path", $output, $ret_val);
exec("unrtf --text ".escapeshellarg($doc_path), $output, $ret_val);
if ($ret_val == 127) { // command not found
return false;
}
@@ -3245,10 +3254,10 @@ class DocumentManager
}
break;
case 'application/vnd.ms-powerpoint':
exec("catppt $doc_path", $output, $ret_val);
exec("catppt ".escapeshellarg($doc_path), $output, $ret_val);
break;
case 'application/vnd.ms-excel':
exec("xls2csv -c\" \" $doc_path", $output, $ret_val);
exec("xls2csv -c\" \" ".escapeshellarg($doc_path), $output, $ret_val);
break;
}
@@ -7168,6 +7177,41 @@ class DocumentManager
);
}
public static function autoResizeImageIfNeeded(int $size, string $tmpName): int
{
$resizeMax = api_get_configuration_value('wysiwyg_image_auto_resize_max');
if (is_array($resizeMax)) {
if ($size > ($resizeMax['mb'] * 1024 * 1024)) {
throw new Exception(get_lang('UplFileTooBig'));
}
$temp = new Image($tmpName);
$pictureInfo = $temp->get_image_info();
$thumbSize = 0;
if ($pictureInfo['width'] > $pictureInfo['height']) {
if ($pictureInfo['width'] > $resizeMax['w']) {
$thumbSize = $resizeMax['w'];
}
} else {
if ($pictureInfo['height'] > $resizeMax['h']) {
$thumbSize = $resizeMax['h'];
}
}
if ($thumbSize) {
$temp->resize($thumbSize);
$temp->send_image($tmpName);
return filesize($tmpName);
}
}
return $size;
}
/**
* Parse file information into a link.
*
+2
View File
@@ -18,8 +18,10 @@ if (file_exists($file)) {
$language = $iso;
}
$questionId = isset($_REQUEST['question_id']) ? (int) $_REQUEST['question_id'] : 0;
$sessionId = api_get_session_id();
$template->assign('question_id', $questionId);
$template->assign('session_id', $sessionId);
$template->assign('elfinder_lang', $language);
$template->assign('elfinder_translation_file', $includeFile);
$template->display('default/javascript/editor/ckeditor/elfinder.tpl');
+11 -382
View File
@@ -2441,6 +2441,8 @@ HOTSPOT;
$courseId = $values['course_id'] ?? 0;
$exerciseId = $values['exercise_id'] ?? 0;
$status = $values['status'] ?? 0;
$questionType = $values['questionType'] ?? ($values['questionTypeId'] ?? 0);
$showAttemptsInSessions = api_get_configuration_value('show_exercise_attempts_in_all_user_sessions');
$whereCondition = '';
if (isset($_GET['filter_by_user']) && !empty($_GET['filter_by_user'])) {
$filter_user = (int) $_GET['filter_by_user'];
@@ -2486,7 +2488,10 @@ HOTSPOT;
false,
false,
true,
$status
$status,
$showAttemptsInSessions,
$questionType,
true
);
if (!empty($result)) {
@@ -3245,9 +3250,6 @@ HOTSPOT;
if (api_is_drh() && !api_is_platform_admin()) {
$delete_link = null;
}
if (api_is_session_admin()) {
$delete_link = '';
}
if ($revised == 3) {
$delete_link = null;
}
@@ -5455,7 +5457,7 @@ EOT;
// Category report
$category_was_added_for_this_test = false;
if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
if (!empty($objQuestionTmp->category)) {
if (!isset($category_list[$objQuestionTmp->category]['score'])) {
$category_list[$objQuestionTmp->category]['score'] = 0;
}
@@ -5493,7 +5495,7 @@ EOT;
$category_list[$objQuestionTmp->category]['total_questions']++;
$category_was_added_for_this_test = true;
}
if (isset($objQuestionTmp->category_list) && !empty($objQuestionTmp->category_list)) {
if (!empty($objQuestionTmp->category_list)) {
foreach ($objQuestionTmp->category_list as $category_id) {
$category_list[$category_id]['score'] += $my_total_score;
$category_list[$category_id]['total'] += $my_total_weight;
@@ -5502,7 +5504,7 @@ EOT;
}
// No category for this question!
if ($category_was_added_for_this_test == false) {
if (!$category_was_added_for_this_test) {
if (!isset($category_list['none']['score'])) {
$category_list['none']['score'] = 0;
}
@@ -6253,6 +6255,8 @@ EOT;
*/
public static function getFeedbackText($message)
{
$message = Security::remove_XSS($message);
return Display::return_message($message, 'warning', false);
}
@@ -7479,381 +7483,6 @@ EOT;
return $output;
}
public static function replaceTermsInContent(string $search, string $replace): array
{
$replacements = [
Database::get_course_table(TABLE_QUIZ_TEST) => [
'iid' => ['title', 'description', 'sound'],
],
Database::get_course_table(TABLE_QUIZ_QUESTION) => [
'iid' => ['question', 'description'],
],
Database::get_course_table(TABLE_QUIZ_ANSWER) => [
'iid' => ['answer', 'comment'],
],
Database::get_course_table(TABLE_ANNOUNCEMENT) => [
'iid' => ['title', 'content'],
],
Database::get_course_table(TABLE_ANNOUNCEMENT_ATTACHMENT) => [
'iid' => ['path', 'comment'],
],
Database::get_course_table(TABLE_ATTENDANCE) => [
'iid' => ['name', 'description', 'attendance_qualify_title'],
],
Database::get_course_table(TABLE_BLOGS) => [
'iid' => ['blog_name', 'blog_subtitle'],
],
Database::get_course_table(TABLE_BLOGS_ATTACHMENT) => [
'iid' => ['path', 'comment'],
],
Database::get_course_table(TABLE_BLOGS_COMMENTS) => [
'iid' => ['title', 'comment'],
],
Database::get_course_table(TABLE_BLOGS_POSTS) => [
'iid' => ['title', 'full_text'],
],
Database::get_course_table(TABLE_BLOGS_TASKS) => [
'iid' => ['title', 'description'],
],
Database::get_course_table(TABLE_AGENDA) => [
'iid' => ['title', 'content', 'comment'],
],
Database::get_course_table(TABLE_AGENDA_ATTACHMENT) => [
'iid' => ['path', 'comment', 'filename'],
],
Database::get_course_table(TABLE_COURSE_DESCRIPTION) => [
'iid' => ['title', 'content'],
],
Database::get_course_table(TABLE_DOCUMENT) => [
'iid' => ['path', 'comment'],
],
Database::get_course_table(TABLE_DROPBOX_FEEDBACK) => [
'iid' => ['feedback'],
],
Database::get_course_table(TABLE_DROPBOX_FILE) => [
'iid' => ['title', 'description'],
],
Database::get_course_table(TABLE_DROPBOX_POST) => [
'iid' => ['feedback'],
],
Database::get_course_table(TABLE_FORUM_ATTACHMENT) => [
'iid' => ['path', 'comment', 'filename'],
],
Database::get_course_table(TABLE_FORUM_CATEGORY) => [
'iid' => ['cat_title', 'cat_comment'],
],
Database::get_course_table(TABLE_FORUM) => [
'iid' => ['forum_title', 'forum_comment', 'forum_image'],
],
Database::get_course_table(TABLE_FORUM_POST) => [
'iid' => ['post_title', 'post_text', 'poster_name'],
],
Database::get_course_table(TABLE_FORUM_THREAD) => [
'iid' => ['thread_title', 'thread_poster_name', 'thread_title_qualify'],
],
Database::get_course_table(TABLE_GLOSSARY) => [
'iid' => ['name', 'description'],
],
Database::get_course_table(TABLE_GROUP_CATEGORY) => [
'iid' => ['title', 'description'],
],
Database::get_course_table(TABLE_GROUP) => [
'iid' => ['name', 'description', 'secret_directory'],
],
Database::get_course_table(TABLE_LINK) => [
'iid' => ['description'],
],
Database::get_course_table(TABLE_LINK_CATEGORY) => [
'iid' => ['category_title', 'description'],
],
Database::get_course_table(TABLE_LP_MAIN) => [
'iid' => ['name', 'ref', 'description', 'path', 'content_license', 'preview_image', 'theme'],
],
Database::get_course_table(TABLE_LP_CATEGORY) => [
'iid' => ['name'],
],
Database::get_course_table(TABLE_LP_ITEM) => [
'iid' => ['prerequisite', 'description', 'title', 'parameters', 'launch_data', 'terms'],
],
Database::get_course_table(TABLE_LP_ITEM_VIEW) => [
'iid' => ['suspend_data', 'lesson_location'],
],
Database::get_course_table(TABLE_NOTEBOOK) => [
'iid' => ['title', 'description'],
],
Database::get_course_table(TABLE_ONLINE_LINK) => [
'iid' => ['name'],
],
Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY) => [
'iid' => ['title', 'description'],
],
Database::get_course_table(TABLE_ROLE) => [
'iid' => ['role_name', 'role_comment'],
],
Database::get_course_table(TABLE_STUDENT_PUBLICATION) => [
'iid' => ['title', 'title_correction', 'description'],
],
Database::get_course_table(TABLE_STUDENT_PUBLICATION_ASSIGNMENT_COMMENT) => [
'iid' => ['comment', 'file'],
],
Database::get_course_table(TABLE_SURVEY) => [
'iid' => ['title', 'subtitle', 'surveythanks', 'invite_mail', 'reminder_mail', 'mail_subject', 'access_condition', 'form_fields'],
],
Database::get_course_table(TABLE_SURVEY_QUESTION_GROUP) => [
'iid' => ['name', 'description'],
],
Database::get_course_table(TABLE_SURVEY_QUESTION) => [
'iid' => ['survey_question', 'survey_question_comment'],
],
Database::get_course_table(TABLE_SURVEY_QUESTION_OPTION) => [
'iid' => ['option_text'],
],
Database::get_course_table(TABLE_THEMATIC) => [
'iid' => ['content', 'title'],
],
Database::get_course_table(TABLE_THEMATIC_ADVANCE) => [
'iid' => ['content'],
],
Database::get_course_table(TABLE_THEMATIC_PLAN) => [
'iid' => ['description'],
],
Database::get_course_table(TABLE_TOOL_LIST) => [
'iid' => ['description'],
],
Database::get_course_table(TABLE_TOOL_INTRO) => [
'iid' => ['intro_text'],
],
Database::get_course_table(TABLE_USER_INFO_DEF) => [
'iid' => ['comment'],
],
Database::get_course_table(TABLE_WIKI) => [
'iid' => ['title', 'content', 'comment', 'progress', 'linksto'],
],
Database::get_course_table(TABLE_WIKI_CONF) => [
'iid' => ['feedback1', 'feedback2', 'feedback3'],
],
Database::get_course_table(TABLE_WIKI_DISCUSS) => [
'iid' => ['comment'],
],
Database::get_main_table(TABLE_CAREER) => [
'id' => ['name', 'description'],
],
Database::get_main_table(TABLE_MAIN_CHAT) => [
'id' => ['message'],
],
Database::get_main_table(TABLE_MAIN_CLASS) => [
'id' => ['name'],
],
Database::get_main_table(TABLE_MAIN_COURSE_REQUEST) => [
'id' => ['description', 'title', 'objetives', 'target_audience'],
],
'course_type' => [
'id' => ['description'],
],
Database::get_main_table(TABLE_EVENT_EMAIL_TEMPLATE) => [
'id' => ['message', 'subject', 'event_type_name'],
],
Database::get_main_table(TABLE_GRADE_MODEL) => [
'id' => ['name', 'description'],
],
Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY) => [
'id' => ['name', 'description'],
],
Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE) => [
'id' => ['path_certificate'],
],
Database::get_main_table(TABLE_MAIN_GRADEBOOK_EVALUATION) => [
'id' => ['description', 'name'],
],
Database::get_main_table(TABLE_MAIN_GRADEBOOK_LINKEVAL_LOG) => [
'id' => ['name', 'description'],
],
Database::get_main_table(TABLE_MAIN_LEGAL) => [
'id' => ['content', 'changes'],
],
Database::get_main_table(TABLE_MESSAGE) => [
'id' => ['content'],
],
Database::get_main_table(TABLE_MESSAGE_ATTACHMENT) => [
'id' => ['path', 'comment', 'filename'],
],
Database::get_main_table(TABLE_NOTIFICATION) => [
'id' => ['content'],
],
Database::get_main_table(TABLE_PERSONAL_AGENDA) => [
'id' => ['title', 'text'],
],
Database::get_main_table(TABLE_PROMOTION) => [
'id' => ['description'],
],
'room' => [
'id' => ['description'],
],
'sequence_condition' => [
'id' => ['description'],
],
'sequence_method' => [
'id' => ['description', 'formula'],
],
'sequence_rule' => [
'id' => ['description'],
],
'sequence_type_entity' => [
'id' => ['description'],
],
'sequence_variable' => [
'id' => ['description'],
],
Database::get_main_table(TABLE_MAIN_SESSION) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_MAIN_SHARED_SURVEY) => [
'survey_id' => ['subtitle', 'surveythanks', 'intro'],
],
Database::get_main_table(TABLE_MAIN_SHARED_SURVEY_QUESTION) => [
'question_id' => ['survey_question', 'survey_question_comment'],
],
Database::get_main_table(TABLE_MAIN_SHARED_SURVEY_QUESTION_OPTION) => [
'question_option_id' => ['option_text'],
],
Database::get_main_table(TABLE_MAIN_SKILL) => [
'id' => ['name', 'description', 'criteria'],
],
Database::get_main_table(TABLE_MAIN_SKILL_PROFILE) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_MAIN_SKILL_REL_USER) => [
'id' => ['argumentation'],
],
'skill_rel_user_comment' => [
'id' => ['feedback_text'],
],
Database::get_main_table(TABLE_MAIN_SYSTEM_ANNOUNCEMENTS) => [
'id' => ['content'],
],
Database::get_main_table(TABLE_MAIN_SYSTEM_CALENDAR) => [
'id' => ['content'],
],
Database::get_main_table(TABLE_MAIN_SYSTEM_TEMPLATE) => [
'id' => ['comment', 'content'],
],
Database::get_main_table(TABLE_MAIN_TEMPLATES) => [
'id' => ['description', 'image'],
],
Database::get_main_table(TABLE_TICKET_CATEGORY) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_TICKET_MESSAGE) => [
'id' => ['message'],
],
Database::get_main_table(TABLE_TICKET_MESSAGE_ATTACHMENTS) => [
'id' => ['filename', 'path'],
],
Database::get_main_table(TABLE_TICKET_PRIORITY) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_TICKET_PROJECT) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_TICKET_STATUS) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_TICKET_TICKET) => [
'id' => ['message'],
],
Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT) => [
'id' => ['answer', 'teacher_comment', 'filename'],
],
Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING) => [
'id' => ['teacher_comment'],
],
Database::get_main_table(TABLE_STATISTIC_TRACK_E_DEFAULT) => [
'default_id' => ['default_value'],
],
Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES) => [
'exe_id' => ['data_tracking', 'questions_to_check'],
],
Database::get_main_table(TABLE_STATISTIC_TRACK_E_ITEM_PROPERTY) => [
'id' => ['content'],
],
'track_e_open' => [
'open_id' => ['open_remote_host', 'open_agent', 'open_referer'],
],
Database::get_main_table(TABLE_TRACK_STORED_VALUES) => [
'id' => ['sv_value'],
],
Database::get_main_table(TABLE_TRACK_STORED_VALUES_STACK) => [
'id' => ['sv_value'],
],
Database::get_main_table(TABLE_MAIN_USER_API_KEY) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_USERGROUP) => [
'id' => ['name', 'description', 'picture', 'url'],
],
Database::get_main_table(TABLE_MAIN_BLOCK) => [
'id' => ['name', 'description', 'path'],
],
];
if (api_get_configuration_value('attendance_allow_comments')) {
$replacements['c_attendance_result_comment'] = [
'iid' => ['comment'],
];
}
if (api_get_configuration_value('exercise_text_when_finished_failure')) {
$replacements[Database::get_course_table(TABLE_QUIZ_TEST)]['iid'][] = 'text_when_finished_failure';
}
$changes = array_map(
fn ($table) => 0,
$replacements
);
foreach ($replacements as $table => $replacement) {
foreach ($replacement as $idColumn => $columns) {
$keys = array_map(fn ($column) => "$column LIKE %?%", $columns);
$values = array_fill(0, count($columns), $search);
$result = Database::select(
[$idColumn, ...$columns],
$table,
[
'where' => [
implode(' OR ', $keys) => $values,
],
'order' => "$idColumn ASC",
]
);
foreach ($result as $row) {
$attributes = array_combine(
$columns,
array_map(
fn ($column) => preg_replace('#'.$search.'#', $replace, $row[$column]),
$columns
)
);
try {
Database::update(
$table,
$attributes,
["$idColumn = ?" => $row[$idColumn]]
);
} catch (Exception $e) {
Database::handleError($e);
}
$changes[$table]++;
}
}
}
return $changes;
}
private static function subscribeSessionWhenFinishedFailure(int $exerciseId): void
{
$failureSession = self::getSessionWhenFinishedFailure($exerciseId);
+49 -55
View File
@@ -5,11 +5,7 @@
use Chamilo\CoreBundle\Component\Editor\Connector;
use Chamilo\CoreBundle\Component\Filesystem\Data;
use Ddeboer\DataImport\Writer\CsvWriter;
use Ddeboer\DataImport\Writer\ExcelWriter;
use MediaAlchemyst\Alchemyst;
use MediaAlchemyst\DriversContainer;
use Neutron\TemporaryFilesystem\Manager;
use Neutron\TemporaryFilesystem\TemporaryFilesystem;
use Symfony\Component\Filesystem\Filesystem;
/**
@@ -77,14 +73,19 @@ class Export
{
$filePath = api_get_path(SYS_ARCHIVE_PATH).uniqid('').'.xlsx';
$file = new \SplFileObject($filePath, 'w');
$writer = new ExcelWriter($file);
@$writer->prepare();
foreach ($data as $row) {
@$writer->writeItem($row);
$excel = @new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$row = 1;
foreach ($data as $item) {
$values = array_values($item);
$count = count($values);
for ($i = 0; $i < $count; $i++) {
@$excel->getActiveSheet()->setCellValueByColumnAndRow($i + 1, $row, $values[$i]);
}
$row++;
}
@$writer->finish();
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($excel, 'Xlsx');
$writer->save($filePath);
DocumentManager::file_send_for_download($filePath, true, $filename.'.xlsx');
exit;
@@ -102,9 +103,8 @@ class Export
$filePath = api_get_path(SYS_ARCHIVE_PATH).uniqid('').'.xlsx';
$file = new \SplFileObject($filePath, 'w');
$excel = @new PHPExcel();
$excel = @new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$type = 'Excel2007';
$sheet = null;
$row = 1;
$prependHeaderRow = false;
@@ -125,7 +125,7 @@ class Export
$headers = array_keys($item);
for ($i = 0; $i < $count; $i++) {
@$excel->getActiveSheet()->setCellValueByColumnAndRow($i, $row, $headers[$i]);
@$excel->getActiveSheet()->setCellValueByColumnAndRow($i + 1, $row, $headers[$i]);
}
$row++;
}
@@ -136,9 +136,9 @@ class Export
if (false !== strpos($values[$i], '[comment]')) {
list($txtValue, $txtComment) = explode('[comment]', $values[$i]);
}
@$excel->getActiveSheet()->setCellValueByColumnAndRow($i, $row, $txtValue);
@$excel->getActiveSheet()->setCellValueByColumnAndRow($i + 1, $row, $txtValue);
if (!empty($txtComment)) {
$columnLetter = PHPExcel_Cell::stringFromColumnIndex($i);
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($i + 1);
$coordinate = $columnLetter.$row;
@$excel->getActiveSheet()->getComment($coordinate)->getText()->createTextRun($txtComment);
}
@@ -146,7 +146,7 @@ class Export
$row++;
}
$writer = \PHPExcel_IOFactory::createWriter($excel, $type);
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($excel, 'Xlsx');
$writer->save($file->getPathname());
DocumentManager::file_send_for_download($filePath, true, $filename.'.xlsx');
@@ -370,51 +370,45 @@ class Export
return false;
}
if (!empty($html)) {
$fs = new Filesystem();
$paths = [
'root_sys' => api_get_path(SYS_PATH),
'path.temp' => api_get_path(SYS_ARCHIVE_PATH),
];
$connector = new Connector();
if (empty($html)) {
return false;
}
$drivers = new DriversContainer();
$drivers['configuration'] = [
'unoconv.binaries' => $unoconv,
'unoconv.timeout' => 60,
];
$fs = new Filesystem();
$paths = [
'root_sys' => api_get_path(SYS_PATH),
'path.temp' => api_get_path(SYS_ARCHIVE_PATH),
];
$connector = new Connector();
$tempFilesystem = TemporaryFilesystem::create();
$manager = new Manager($tempFilesystem, $fs);
$alchemyst = new Alchemyst($drivers, $manager);
$dataFileSystem = new Data($paths, $fs, $connector, $unoconv);
$content = $dataFileSystem->convertRelativeToAbsoluteUrl($html);
// convertRelativeToAbsoluteUrl returns a simple_html_dom object; cast to string explicitly
$content = (string) $content;
$dataFileSystem = new Data($paths, $fs, $connector, $alchemyst);
$content = $dataFileSystem->convertRelativeToAbsoluteUrl($html);
$filePath = $dataFileSystem->putContentInTempFile(
$content,
api_replace_dangerous_char($name),
'html'
);
$safeName = api_replace_dangerous_char($name);
$filePath = $dataFileSystem->putContentInTempFile($content, $safeName, 'html');
$try = true;
if (empty($filePath)) {
error_log("htmlToOdt: failed to create temp file for '$name'");
while ($try) {
try {
$convertedFile = $dataFileSystem->transcode(
$filePath,
$format
);
return false;
}
$try = false;
DocumentManager::file_send_for_download(
$convertedFile,
false,
$name.'.'.$format
);
} catch (Exception $e) {
// error_log($e->getMessage());
}
try {
$convertedFile = $dataFileSystem->transcode($filePath, $format);
if ($convertedFile) {
DocumentManager::file_send_for_download(
$convertedFile,
true,
$name.'.'.$format
);
} else {
error_log("htmlToOdt: transcode failed for '$name'");
}
} catch (Exception $e) {
error_log("htmlToOdt: exception during transcode for '$name': ".$e->getMessage());
}
}
}
+79 -8
View File
@@ -789,7 +789,7 @@ class ExtraField extends Model
$setData = [];
foreach ($showOnlyTheseFields as $variable) {
$extraName = 'extra_'.$variable;
if (in_array($extraName, array_keys($extraData))) {
if (array_key_exists($extraName, $extraData)) {
$setData[$extraName] = $extraData[$extraName];
}
}
@@ -824,13 +824,40 @@ class ExtraField extends Model
$help
);
if (!empty($requiredFields)) {
/** @var HTML_QuickForm_input $element */
foreach ($form->getElements() as $element) {
$name = str_replace('extra_', '', $element->getName());
if (in_array($name, $requiredFields)) {
$form->setRequired($element);
}
// Ensure $requiredFields is an array
$requiredFields = is_array($requiredFields) ? $requiredFields : [];
// Fetch the configured “unique extra field” var
$uniqueField = api_get_configuration_value('extra_field_to_validate_on_user_registration');
// Determine if were editing an existing user (item_id present) or creating a new one
$currentUserId = $form->getElementValue('item_id') ?: null;
// Always mark the unique extra field as required on user forms
if (
$this->type === 'user'
&& !empty($uniqueField)
&& !in_array($uniqueField, $requiredFields, true)
) {
$requiredFields[] = $uniqueField;
}
/** @var HTML_QuickForm_element $element */
foreach ($form->getElements() as $element) {
// Strip the “extra_” prefix to get the field name
$name = str_replace('extra_', '', $element->getName());
// 1) Mark as required if configured
if (in_array($name, $requiredFields, true)) {
$form->setRequired($element);
}
// 2) If this is the special extra field on a user form, add uniqueness validation
if ($this->type === 'user' && !empty($uniqueField) && $name === $uniqueField) {
$this->applyExtraFieldUniquenessRule(
$form,
$name,
$uniqueField,
$currentUserId
);
}
}
@@ -3277,6 +3304,50 @@ JAVASCRIPT;
return null;
}
/**
* Add the “unique per URL” validation rule for the extrafield.
*
* @param &$form
* @param string $fieldVar The extrafield variable name (without “extra_”)
* @param string $uniqueField Configured unique field var
* @param int|null $currentUserId Null for creation, or the existing user ID when editing
*/
protected function applyExtraFieldUniquenessRule(&$form, string $fieldVar, string $uniqueField, ?int $currentUserId): void
{
// Only apply if this is the configured unique field
if ($fieldVar !== $uniqueField) {
return;
}
$elementName = 'extra_'.$fieldVar;
$message = sprintf(
get_lang('A user with the same %s already exists in this portal'),
$fieldVar
);
if ($currentUserId === null) {
// Creation: forbid any existing match
$form->addRule(
$elementName,
$message,
'callback',
['UserManager', 'isExtraFieldValueUniquePerUrl']
);
} else {
// Editing: allow if the only match is this same user
$form->addRule(
$elementName,
$message,
'callback',
function (string $value) use ($currentUserId) {
$existingId = UserManager::isExtraFieldValueUniquePerUrl($value, true);
return $existingId === null || $existingId == $currentUserId;
}
);
}
}
/**
* @param \FormValidator $form
* @param int $defaultValueId
+21
View File
@@ -1221,4 +1221,25 @@ class ExtraFieldValue extends Model
return true;
}
/**
* @return array<int, array<string, string>>
*/
public static function formatValues(array $extraInfo): array
{
$formatted = [];
foreach ($extraInfo as $extra) {
/** @var ExtraFieldValues $extraValue */
$extraValue = $extra['value'];
$formatted[] = [
'variable' => $extraValue->getField()->getVariable(),
'display_text' => $extraValue->getField()->getDisplayText(),
'value' => $extraValue->getValue(),
];
}
return $formatted;
}
}
+8 -4
View File
@@ -212,7 +212,7 @@ function move($source, $target, $forceMove = true, $moveContent = false)
if (is_file($source)) {
if ($forceMove) {
if (!$isWindowsOS && $canExec) {
exec('mv '.$source.' '.$target.'/'.$file_name);
exec('mv '.escapeshellarg($source).' '.escapeshellarg($target.'/'.$file_name));
} else {
// Try copying
copy($source, $target.'/'.$file_name);
@@ -235,15 +235,19 @@ function move($source, $target, $forceMove = true, $moveContent = false)
$base = basename($source);
$out = [];
$retVal = -1;
exec('mv '.$source.'/* '.$target.'/'.$base, $out, $retVal);
exec(
'find '.escapeshellarg($source).' -mindepth 1 -maxdepth 1 -exec mv -t '.escapeshellarg($target.'/'.$base).' {} +',
$out,
$retVal
);
if ($retVal !== 0) {
return false; // mv should return 0 on success
}
exec('rm -rf '.$source);
exec('rm -rf '.escapeshellarg($source));
} else {
$out = [];
$retVal = -1;
exec("mv $source $target", $out, $retVal);
exec('mv '.escapeshellarg($source).' '.escapeshellarg($target), $out, $retVal);
if ($retVal !== 0) {
error_log("Chamilo error fileManage.lib.php: mv $source $target\n");
+1 -1
View File
@@ -26,7 +26,7 @@ use enshrined\svgSanitize\Sanitizer;
*/
function php2phps($file_name)
{
return preg_replace('/\.(phar.?|php.?|phtml.?)(\.){0,1}.*$/i', '.phps', $file_name);
return preg_replace('/\.(phar.?|php.?|pht.?|phtml.?)(\.){0,1}.*$/i', '.phps', $file_name);
}
/**
+9 -1
View File
@@ -1427,9 +1427,16 @@ class GroupManager
return false;
}
$session_id = api_get_session_id();
$studentStatus = 5;
if (isset($session_id) && $session_id != 0) {
$studentStatus = 0;
}
$complete_user_list = CourseManager::get_user_list_from_course_code(
$_course['code'],
$session_id
$session_id,
null,
null,
$studentStatus
);
$groupIid = $groupInfo['iid'];
$category = self::get_category_from_group($groupIid);
@@ -1448,6 +1455,7 @@ class GroupManager
}
$usersToAdd = [];
shuffle($complete_user_list);
foreach ($complete_user_list as $userInfo) {
$isSubscribed = self::is_subscribed($userInfo['user_id'], $groupInfo);
if ($isSubscribed) {
+51
View File
@@ -33,6 +33,8 @@ class Image
exit;
}
}
$this->image_wrapper->reencode();
}
public function resize($max_size_for_picture)
@@ -157,6 +159,7 @@ abstract class ImageWrapper
}
$this->path = $path;
$this->set_image_wrapper(); //Creates image obj
$this->reencode();
}
abstract public function set_image_wrapper();
@@ -169,6 +172,8 @@ abstract class ImageWrapper
abstract public function crop($x, $y, $width, $height, $src_width, $src_height);
abstract public function reencode(): void;
abstract public function send_image($file = '', $compress = -1, $convert_file_to = null);
/**
@@ -347,6 +352,27 @@ class ImagickWrapper extends ImageWrapper
return $result;
}
}
/**
* @throws Exception
*/
public function reencode(): void
{
if ($this->image_validated) {
try {
$this->image->setImageFormat($this->type);
$this->image->writeImage($this->path);
} catch (ImagickException $e) {
throw new Exception();
}
$this->image->clear();
return;
}
throw new Exception();
}
}
/**
@@ -482,6 +508,31 @@ class GDWrapper extends ImageWrapper
@imagedestroy($src_img);
}
/**
* @throws Exception
*/
public function reencode(): void
{
if (!$this->image_validated) {
return;
}
switch ($this->type) {
case 'jpeg':
case 'jpg':
imagejpeg($this->bg, $this->path);
break;
case 'png':
imagepng($this->bg, $this->path);
break;
case 'gif':
imagegif($this->bg, $this->path);
break;
}
}
/**
* @author José Loguercio <jose.loguercio@beeznest.com>
*
+31 -14
View File
@@ -714,19 +714,36 @@ function api_format_date($time, $format = null, $language = null)
//$date_formatter->setPattern($date_format);
$formatted_date = api_to_system_encoding($date_formatter->format($time), 'UTF-8');
} else {
// We replace %a %A %b %B masks of date format with translated strings
// Format date without strftime() (deprecated PHP 8.1, removed PHP 9.0).
// Process each %X token individually so date() never sees translated
// day/month names (which contain letters it would misinterpret as codes).
$translated = &_api_get_day_month_names($language);
$date_format = str_replace(
['%A', '%a', '%B', '%b'],
[
$translated['days_long'][(int) strftime('%w', $time)],
$translated['days_short'][(int) strftime('%w', $time)],
$translated['months_long'][(int) strftime('%m', $time) - 1],
$translated['months_short'][(int) strftime('%m', $time) - 1],
],
$date_format
);
$formatted_date = api_to_system_encoding(strftime($date_format, $time), 'UTF-8');
$dayOfWeek = (int) date('w', $time);
$monthIndex = (int) date('n', $time) - 1;
$strfToDate = [
'd' => 'd', 'm' => 'm', 'Y' => 'Y', 'y' => 'y',
'H' => 'H', 'I' => 'h', 'M' => 'i', 'S' => 's',
'p' => 'A', 'P' => 'a', 'e' => 'j', 'w' => 'w',
'u' => 'N',
];
$formatted_date = '';
for ($fi = 0, $flen = strlen($date_format); $fi < $flen; $fi++) {
if ($date_format[$fi] !== '%' || $fi + 1 >= $flen) {
$formatted_date .= $date_format[$fi];
continue;
}
$code = $date_format[++$fi];
switch ($code) {
case 'A': $formatted_date .= $translated['days_long'][$dayOfWeek]; break;
case 'a': $formatted_date .= $translated['days_short'][$dayOfWeek]; break;
case 'B': $formatted_date .= $translated['months_long'][$monthIndex]; break;
case 'b': $formatted_date .= $translated['months_short'][$monthIndex]; break;
case '%': $formatted_date .= '%'; break;
default:
$formatted_date .= isset($strfToDate[$code]) ? date($strfToDate[$code], $time) : '%'.$code;
}
}
$formatted_date = api_to_system_encoding($formatted_date, 'UTF-8');
}
date_default_timezone_set($system_timezone);
@@ -1052,7 +1069,7 @@ function api_sort_by_first_name($language = null)
* 'quarter_end' => '2022-12-31',
* 'quarter_title' => 'Q4 2022']
*/
function getQuarterDates(string $date = null): array
function getQuarterDates(?string $date = null): array
{
if (empty($date)) {
$date = api_get_utc_datetime();
@@ -1207,7 +1224,7 @@ function api_htmlentities($string, $quote_style = ENT_COMPAT, $encoding = 'UTF-8
break;
}
return htmlentities($string, ENT_SUBSTITUTE, 'UTF-8');
return mb_encode_numericentity($string, [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8');
}
/**
+1 -1
View File
@@ -1840,7 +1840,7 @@ class Link extends Model
);
}
$client = new Client(['defaults' => $defaults]);
$client = new Client($defaults);
try {
$responseIpv6 = $client->get($url);
+74 -11
View File
@@ -37,9 +37,9 @@ class Login
if ($reset) {
if ($by_username) {
$secret_word = self::get_secret_word($user['email']);
$token = self::generate_reset_token($user['uid']);
if ($reset) {
$reset_link = $portal_url."main/auth/lostPassword.php?reset=".$secret_word."&id=".$user['uid'];
$reset_link = $portal_url."main/auth/lostPassword.php?reset=".$token."&id=".$user['uid'];
$reset_link = Display::url($reset_link, $reset_link);
} else {
$reset_link = get_lang('Pass')." : $user[password]";
@@ -53,9 +53,9 @@ class Login
}
} else {
foreach ($user as $this_user) {
$secret_word = self::get_secret_word($this_user['email']);
$token = self::generate_reset_token($this_user['uid']);
if ($reset) {
$reset_link = $portal_url."main/auth/lostPassword.php?reset=".$secret_word."&id=".$this_user['uid'];
$reset_link = $portal_url."main/auth/lostPassword.php?reset=".$token."&id=".$this_user['uid'];
$reset_link = Display::url($reset_link, $reset_link);
} else {
$reset_link = get_lang('Pass')." : $this_user[password]";
@@ -248,9 +248,36 @@ class Login
*
* @author Olivier Cauberghe <olivier.cauberghe@UGent.be>, Ghent University
*/
/**
* Generate a cryptographically random reset token for the given user,
* store it (with a timestamp) in the database, and return it.
*
* @param int $userId
*
* @return string The hex token
*/
public static function generate_reset_token($userId)
{
$token = bin2hex(random_bytes(32));
$em = Database::getManager();
/** @var User $user */
$user = $em->find('ChamiloUserBundle:User', (int) $userId);
if ($user) {
$user->setConfirmationToken($token);
$user->setPasswordRequestedAt(new \DateTime());
$em->persist($user);
$em->flush();
}
return $token;
}
/**
* @deprecated Use generate_reset_token() instead.
*/
public static function get_secret_word($add)
{
return $secret_word = sha1($add);
return sha1($add);
}
/**
@@ -285,15 +312,51 @@ class Login
return get_lang('CouldNotResetPassword');
}
if (self::get_secret_word($user['email']) == $secret) {
// OK, secret word is good. Now change password and mail it.
$user['password'] = api_generate_password();
UserManager::updatePassword($id, $user['password']);
// Validate token against the stored confirmation_token.
$em = Database::getManager();
/** @var User $dbUser */
$dbUser = $em->find('ChamiloUserBundle:User', $id);
return self::send_password_to_user($user, $by_username);
if (!$dbUser) {
return get_lang('CouldNotResetPassword');
}
return get_lang('NotAllowed');
$storedToken = $dbUser->getConfirmationToken();
$requestedAt = $dbUser->getPasswordRequestedAt();
// Token must exist (a reset must have been requested first).
if (empty($storedToken) || empty($requestedAt)) {
return get_lang('NotAllowed');
}
// Token expires after 1 hour.
$expiresAt = clone $requestedAt;
$expiresAt->modify('+1 hour');
if (new \DateTime() > $expiresAt) {
// Clear expired token.
$dbUser->setConfirmationToken(null);
$dbUser->setPasswordRequestedAt(null);
$em->persist($dbUser);
$em->flush();
return get_lang('NotAllowed');
}
// Timing-safe comparison.
if (!hash_equals($storedToken, $secret)) {
return get_lang('NotAllowed');
}
// Token is valid — change the password and clear the token.
$user['password'] = api_generate_password();
UserManager::updatePassword($id, $user['password']);
$dbUser->setConfirmationToken(null);
$dbUser->setPasswordRequestedAt(null);
$em->persist($dbUser);
$em->flush();
return self::send_password_to_user($user, $by_username);
}
/**
+4
View File
@@ -1097,6 +1097,10 @@ class MessageManager
$fileCopied = true;
}
}
if ('image/svg+xml' === $type) {
sanitizeSvgFile($new_path);
}
}
if ($fileCopied) {
+348 -9
View File
@@ -13,6 +13,7 @@ use Exception;
*/
abstract class ActivityExport
{
public const DOCS_MODULE_ID = 0;
protected $course;
public function __construct($course)
@@ -27,15 +28,44 @@ abstract class ActivityExport
abstract public function export($activityId, $exportDir, $moduleId, $sectionId);
/**
* Get the section ID for a given activity ID.
* Get the section ID (learnpath source_id) for a given activity.
*/
public function getSectionIdForActivity(int $activityId, string $itemType): int
{
if (empty($this->course->resources[RESOURCE_LEARNPATH])) {
return 0;
}
foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) {
if (empty($learnpath->items)) {
continue;
}
foreach ($learnpath->items as $item) {
$item['item_type'] = $item['item_type'] === 'student_publication' ? 'work' : $item['item_type'];
if ($item['item_type'] == $itemType && $item['path'] == $activityId) {
return $learnpath->source_id;
$normalizedType = $item['item_type'] === 'student_publication'
? 'work'
: $item['item_type'];
if ($normalizedType !== $itemType) {
continue;
}
// Classic case: LP stores the numeric id in "path"
if (ctype_digit((string) $item['path']) && (int) $item['path'] === $activityId) {
return (int) $learnpath->source_id;
}
// Fallback for documents when LP stores the path instead of the id
if ($itemType === RESOURCE_DOCUMENT) {
$doc = \DocumentManager::get_document_data_by_id($activityId, $this->course->code);
if (!empty($doc['path'])) {
$p = (string) $doc['path'];
foreach ([$p, 'document/'.$p, '/'.$p] as $candidate) {
if ((string) $item['path'] === $candidate) {
return (int) $learnpath->source_id;
}
}
}
}
}
}
@@ -116,27 +146,29 @@ abstract class ActivityExport
*/
protected function createInforefXml(array $references, string $directory): void
{
// Start the XML content
$xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
$xmlContent .= '<inforef>'.PHP_EOL;
// Add user references if provided
if (isset($references['users']) && is_array($references['users'])) {
$xmlContent .= ' <userref>'.PHP_EOL;
foreach ($references['users'] as $userId) {
$xmlContent .= ' <user>'.PHP_EOL;
$xmlContent .= ' <id>'.htmlspecialchars($userId).'</id>'.PHP_EOL;
$xmlContent .= ' <id>'.htmlspecialchars((string) $userId).'</id>'.PHP_EOL;
$xmlContent .= ' </user>'.PHP_EOL;
}
$xmlContent .= ' </userref>'.PHP_EOL;
}
// Add file references if provided
if (isset($references['files']) && is_array($references['files'])) {
$xmlContent .= ' <fileref>'.PHP_EOL;
foreach ($references['files'] as $file) {
$fileId = is_array($file) ? (int) ($file['id'] ?? 0) : (int) $file;
if ($fileId <= 0) {
continue;
}
$xmlContent .= ' <file>'.PHP_EOL;
$xmlContent .= ' <id>'.htmlspecialchars($file['id']).'</id>'.PHP_EOL;
$xmlContent .= ' <id>'.$fileId.'</id>'.PHP_EOL;
$xmlContent .= ' </file>'.PHP_EOL;
}
$xmlContent .= ' </fileref>'.PHP_EOL;
@@ -252,4 +284,311 @@ abstract class ActivityExport
$this->createXmlFile('calendar', $xmlContent, $destinationDir);
}
/**
* Creates a Moodle-safe activity name.
* - Strip HTML
* - Decode entities
* - Normalize whitespace
* - Truncate to a maximum length.
*/
protected function sanitizeMoodleActivityName(string $raw, int $maxLen = 255): string
{
$s = trim($raw);
if ($s === '') {
return '';
}
$s = html_entity_decode($s, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$s = strip_tags($s);
$s = preg_replace('/\s+/u', ' ', $s);
$s = trim($s);
if ($s === '') {
return '';
}
if (function_exists('mb_strlen') && function_exists('mb_substr')) {
if (mb_strlen($s, 'UTF-8') > $maxLen) {
$s = mb_substr($s, 0, $maxLen, 'UTF-8');
}
} else {
if (strlen($s) > $maxLen) {
$s = substr($s, 0, $maxLen);
}
}
return $s;
}
/**
* Returns the title of the item in the LP if it exists, otherwise the fallback.
*/
protected function lpItemTitle(int $sectionId, string $itemType, int $resourceId, ?string $fallback): string
{
if (!isset($this->course->resources[RESOURCE_LEARNPATH])) {
return $fallback ?? '';
}
foreach ($this->course->resources[RESOURCE_LEARNPATH] as $lp) {
if ((int) $lp->source_id !== $sectionId || empty($lp->items)) {
continue;
}
foreach ($lp->items as $it) {
$type = $it['item_type'] === 'student_publication' ? 'work' : $it['item_type'];
if ($type === $itemType && (int) $it['path'] === $resourceId) {
return $it['title'] ?? ($fallback ?? '');
}
}
}
return $fallback ?? '';
}
/**
* Extract embedded files from HTML content and normalize URLs to @@PLUGINFILE@@.
*
* Supported sources:
* - Course documents: /document/...
* - Platform assets: /main/default_course_document/... and /main/img/...
*
* @param callable $fileIdBuilder Receives the 1-based sequence and must return a unique file id.
*
* @return array{content:string, files:array<int,array<string,mixed>>}
*/
protected function extractEmbeddedFilesAndNormalizeContent(
string $html,
int $contextId,
string $component,
string $fileArea,
int $itemId,
callable $fileIdBuilder
): array {
if ($html === '') {
return [
'content' => '',
'files' => [],
];
}
$courseInfo = api_get_course_info($this->course->code);
$adminId = (int) (MoodleExport::getAdminUserData()['id'] ?? 1);
$fileExport = new FileExport($this->course);
$files = [];
$seenSources = [];
$sequence = 0;
$normalizedHtml = preg_replace_callback(
'#<img[^>]+src=["\'](?<url>[^"\']+)["\']#i',
function ($match) use (
$courseInfo,
$contextId,
$component,
$fileArea,
$itemId,
$fileIdBuilder,
$adminId,
$fileExport,
&$files,
&$seenSources,
&$sequence
) {
$src = (string) ($match['url'] ?? '');
$resolved = $this->resolveEmbeddedFileSource($src, $courseInfo);
if ($resolved === null) {
return $match[0];
}
$sourceKey = (string) $resolved['sourcekey'];
if (!isset($seenSources[$sourceKey])) {
$sequence++;
$fileId = (int) call_user_func($fileIdBuilder, $sequence);
$absolutePath = (string) $resolved['absolutepath'];
$filename = (string) $resolved['filename'];
$files[] = [
'id' => $fileId,
'contenthash' => is_file($absolutePath)
? sha1_file($absolutePath)
: hash('sha1', $filename),
'contextid' => $contextId,
'component' => $component,
'filearea' => $fileArea,
'itemid' => $itemId,
'filepath' => (string) $resolved['filepath'],
'documentpath' => (string) $resolved['documentpath'],
'absolutepath' => $absolutePath,
'filename' => $filename,
'userid' => $adminId,
'filesize' => is_file($absolutePath) ? (int) filesize($absolutePath) : 0,
'mimetype' => $fileExport->getMimeType($filename),
'status' => 0,
'timecreated' => time() - 3600,
'timemodified' => time(),
'source' => $filename,
'author' => 'Unknown',
'license' => 'allrightsreserved',
];
$seenSources[$sourceKey] = true;
}
return str_replace($src, '@@PLUGINFILE@@'.(string) $resolved['pluginfilepath'], $match[0]);
},
$html
);
return [
'content' => (string) $normalizedHtml,
'files' => $files,
];
}
/**
* Resolve an embeddable source URL into export metadata.
*
* @return array<string,string>|null
*/
protected function resolveEmbeddedFileSource(string $src, array $courseInfo): ?array
{
$urlPath = $this->extractUrlPath($src);
if (preg_match('#/document(?P<path>/[^"\']+)#', $urlPath, $m)) {
$documentRelativePath = (string) $m['path'];
$docId = \DocumentManager::get_document_id($courseInfo, $documentRelativePath);
if (empty($docId)) {
return null;
}
$document = \DocumentManager::get_document_data_by_id((int) $docId, $this->course->code);
if (empty($document)) {
return null;
}
$documentPath = (string) ($document['path'] ?? '');
if ($documentPath === '') {
return null;
}
return [
'sourcekey' => 'document:'.$docId,
'filepath' => $this->buildPluginFileDirectoryFromChamiloDocumentPath($documentPath),
'pluginfilepath' => $this->buildPluginFilePathFromChamiloDocumentPath($documentRelativePath),
'documentpath' => 'document'.$documentPath,
'absolutepath' => $this->buildChamiloDocumentAbsolutePath($documentPath),
'filename' => basename($documentPath),
];
}
if (preg_match('#(?P<path>/main/(?:default_course_document|img)/[^"\']+)#', $urlPath, $m)) {
$assetPath = (string) $m['path'];
return [
'sourcekey' => 'static:'.$assetPath,
'filepath' => $this->buildPluginFileDirectoryFromStaticAssetPath($assetPath),
'pluginfilepath' => $this->buildPluginFilePathFromStaticAssetPath($assetPath),
'documentpath' => '',
'absolutepath' => $this->buildStaticAssetAbsolutePath($assetPath),
'filename' => basename($assetPath),
];
}
return null;
}
/**
* Extract the path part from a raw URL.
*/
protected function extractUrlPath(string $src): string
{
$decoded = html_entity_decode($src, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$path = (string) parse_url($decoded, PHP_URL_PATH);
return $path !== '' ? $path : $decoded;
}
/**
* Build the absolute path for one Chamilo course document.
*/
protected function buildChamiloDocumentAbsolutePath(string $documentPath): string
{
$normalizedPath = (string) preg_replace('#^/?document/#', '', str_replace('\\', '/', trim($documentPath)));
$normalizedPath = '/'.ltrim($normalizedPath, '/');
return rtrim((string) $this->course->path, '/').'/document'.$normalizedPath;
}
/**
* Build the absolute path for one static platform asset.
*/
protected function buildStaticAssetAbsolutePath(string $assetPath): string
{
return rtrim(api_get_path(SYS_PATH), '/').'/'.ltrim($assetPath, '/');
}
/**
* Build the pluginfile directory path from a Chamilo document path.
*/
protected function buildPluginFileDirectoryFromChamiloDocumentPath(string $documentPath): string
{
$relative = $this->stripChamiloDocumentPrefix($documentPath);
$relative = ltrim(str_replace('\\', '/', $relative), '/');
$dir = dirname($relative);
if ($dir === '.' || $dir === '/') {
return '/';
}
return '/'.trim($dir, '/').'/';
}
/**
* Build the pluginfile full path used in HTML content.
*/
protected function buildPluginFilePathFromChamiloDocumentPath(string $documentPath): string
{
$relative = $this->stripChamiloDocumentPrefix($documentPath);
$relative = ltrim(str_replace('\\', '/', $relative), '/');
return '/'.$relative;
}
/**
* Build the pluginfile directory path for one static platform asset.
*/
protected function buildPluginFileDirectoryFromStaticAssetPath(string $assetPath): string
{
$relative = ltrim(str_replace('\\', '/', trim($assetPath)), '/');
$dir = dirname($relative);
if ($dir === '.' || $dir === '/') {
return '/Static/';
}
return '/Static/'.trim($dir, '/').'/';
}
/**
* Build the pluginfile full path used in HTML content for one static platform asset.
*/
protected function buildPluginFilePathFromStaticAssetPath(string $assetPath): string
{
$relative = ltrim(str_replace('\\', '/', trim($assetPath)), '/');
return '/Static/'.$relative;
}
/**
* Remove the internal Chamilo "document/" prefix if present.
*/
protected function stripChamiloDocumentPrefix(string $path): string
{
$path = str_replace('\\', '/', trim($path));
$path = preg_replace('#^/?document/#', '', $path);
return (string) $path;
}
}
+24 -12
View File
@@ -55,8 +55,12 @@ class CourseExport
*/
private function createCourseXml(string $destinationDir): void
{
$courseId = $this->courseInfo['real_id'] ?? 0;
$contextId = $this->courseInfo['real_id'] ?? 1;
$courseId = (int) ($this->courseInfo['real_id'] ?? 0);
$contextId = (int) MoodleExport::getBackupCourseContextId();
if ($contextId <= 0) {
$contextId = 700000000 + max(1, $courseId);
}
$shortname = $this->courseInfo['code'] ?? 'Unknown Course';
$fullname = $this->courseInfo['title'] ?? 'Unknown Fullname';
$showgrades = $this->courseInfo['showgrades'] ?? 0;
@@ -153,15 +157,24 @@ class CourseExport
$questionCategories = [];
foreach ($this->activities as $activity) {
if ($activity['modulename'] === 'quiz') {
$quizExport = new QuizExport($this->course);
$quizData = $quizExport->getData($activity['id'], $activity['sectionid']);
foreach ($quizData['questions'] as $question) {
$categoryId = $question['questioncategoryid'];
if (!in_array($categoryId, $questionCategories, true)) {
$questionCategories[] = $categoryId;
}
}
if (($activity['modulename'] ?? '') !== 'quiz') {
continue;
}
$quizExport = new QuizExport($this->course);
$quizData = $quizExport->getData(
(int) ($activity['id'] ?? 0),
(int) ($activity['sectionid'] ?? 0),
(int) ($activity['moduleid'] ?? 0)
);
if (empty($quizData)) {
continue;
}
$categoryId = (int) ($quizData['question_category_id'] ?? 0);
if ($categoryId > 0 && !in_array($categoryId, $questionCategories, true)) {
$questionCategories[] = $categoryId;
}
}
@@ -175,7 +188,6 @@ class CourseExport
$xmlContent .= ' </question_categoryref>'.PHP_EOL;
}
// Add role references
$xmlContent .= ' <roleref>'.PHP_EOL;
$xmlContent .= ' <role>'.PHP_EOL;
$xmlContent .= ' <id>5</id>'.PHP_EOL;
+113 -95
View File
@@ -38,20 +38,17 @@ class FileExport
mkdir($filesDir, api_get_permissions_for_new_directories(), true);
}
// Create placeholder index.html
$this->createPlaceholderFile($filesDir);
// Export each file
foreach ($filesData['files'] as $file) {
$this->copyFileToExportDir($file, $filesDir);
}
// Create files.xml in the export directory
$this->createFilesXml($filesData, $exportDir);
}
/**
* Get file data from course resources. This is for testing purposes.
* Get file data from course resources.
*/
public function getFilesData(): array
{
@@ -99,6 +96,17 @@ class FileExport
return $filesData;
}
/**
* Get MIME type based on the file extension.
*/
public function getMimeType($filePath): string
{
$extension = strtolower((string) pathinfo((string) $filePath, PATHINFO_EXTENSION));
$mimeTypes = $this->getMimeTypes();
return $mimeTypes[$extension] ?? 'application/octet-stream';
}
/**
* Create a placeholder index.html file to prevent an empty directory.
*/
@@ -113,27 +121,38 @@ class FileExport
*/
private function copyFileToExportDir(array $file, string $filesDir): void
{
if ($file['filepath'] === '.') {
if (($file['filepath'] ?? '') === '.') {
return;
}
$contenthash = (string) ($file['contenthash'] ?? '');
if ($contenthash === '') {
return;
}
$contenthash = $file['contenthash'];
$subDir = substr($contenthash, 0, 2);
$filePath = $this->course->path.$file['documentpath'];
$exportSubDir = $filesDir.'/'.$subDir;
// Ensure the subdirectory exists
if (!is_dir($exportSubDir)) {
mkdir($exportSubDir, api_get_permissions_for_new_directories(), true);
}
// Copy the file to the export directory
$destinationFile = $exportSubDir.'/'.$contenthash;
if (file_exists($filePath)) {
copy($filePath, $destinationFile);
$filePath = '';
if (!empty($file['absolutepath'])) {
$filePath = (string) $file['absolutepath'];
} else {
throw new Exception("File {$filePath} not found.");
$filePath = $this->course->path.($file['documentpath'] ?? '');
}
if (is_file($filePath)) {
copy($filePath, $destinationFile);
return;
}
throw new Exception("File {$filePath} not found.");
}
/**
@@ -152,28 +171,56 @@ class FileExport
file_put_contents($destinationDir.'/files.xml', $xmlContent);
}
/**
* Ensure mandatory file fields exist for Moodle restore.
*/
private function normalizeFileEntry(array $file): array
{
$adminData = MoodleExport::getAdminUserData();
$adminId = (int) ($adminData['id'] ?? 1);
if (!isset($file['itemid']) || $file['itemid'] === null || (is_string($file['itemid']) && trim($file['itemid']) === '')) {
$file['itemid'] = 0;
} else {
$file['itemid'] = (int) $file['itemid'];
}
if (!isset($file['userid']) || $file['userid'] === null || (is_string($file['userid']) && trim($file['userid']) === '')) {
$file['userid'] = $adminId;
} else {
$file['userid'] = (int) $file['userid'];
}
return $file;
}
/**
* Create an XML entry for a file.
*/
private function createFileXmlEntry(array $file): string
{
$file = $this->normalizeFileEntry($file);
$itemId = (int) $file['itemid'];
$userId = (int) $file['userid'];
return ' <file id="'.$file['id'].'">'.PHP_EOL.
' <contenthash>'.htmlspecialchars($file['contenthash']).'</contenthash>'.PHP_EOL.
' <contenthash>'.htmlspecialchars((string) $file['contenthash']).'</contenthash>'.PHP_EOL.
' <contextid>'.$file['contextid'].'</contextid>'.PHP_EOL.
' <component>'.htmlspecialchars($file['component']).'</component>'.PHP_EOL.
' <filearea>'.htmlspecialchars($file['filearea']).'</filearea>'.PHP_EOL.
' <itemid>0</itemid>'.PHP_EOL.
' <filepath>'.htmlspecialchars($file['filepath']).'</filepath>'.PHP_EOL.
' <filename>'.htmlspecialchars($file['filename']).'</filename>'.PHP_EOL.
' <userid>'.$file['userid'].'</userid>'.PHP_EOL.
' <component>'.htmlspecialchars((string) $file['component']).'</component>'.PHP_EOL.
' <filearea>'.htmlspecialchars((string) $file['filearea']).'</filearea>'.PHP_EOL.
' <itemid>'.$itemId.'</itemid>'.PHP_EOL.
' <filepath>'.htmlspecialchars((string) $file['filepath']).'</filepath>'.PHP_EOL.
' <filename>'.htmlspecialchars((string) $file['filename']).'</filename>'.PHP_EOL.
' <userid>'.$userId.'</userid>'.PHP_EOL.
' <filesize>'.$file['filesize'].'</filesize>'.PHP_EOL.
' <mimetype>'.htmlspecialchars($file['mimetype']).'</mimetype>'.PHP_EOL.
' <mimetype>'.htmlspecialchars((string) $file['mimetype']).'</mimetype>'.PHP_EOL.
' <status>'.$file['status'].'</status>'.PHP_EOL.
' <timecreated>'.$file['timecreated'].'</timecreated>'.PHP_EOL.
' <timemodified>'.$file['timemodified'].'</timemodified>'.PHP_EOL.
' <source>'.htmlspecialchars($file['source']).'</source>'.PHP_EOL.
' <author>'.htmlspecialchars($file['author']).'</author>'.PHP_EOL.
' <license>'.htmlspecialchars($file['license']).'</license>'.PHP_EOL.
' <source>'.htmlspecialchars((string) $file['source']).'</source>'.PHP_EOL.
' <author>'.htmlspecialchars((string) $file['author']).'</author>'.PHP_EOL.
' <license>'.htmlspecialchars((string) $file['license']).'</license>'.PHP_EOL.
' <sortorder>0</sortorder>'.PHP_EOL.
' <repositorytype>$@NULL@$</repositorytype>'.PHP_EOL.
' <repositoryid>$@NULL@$</repositoryid>'.PHP_EOL.
@@ -186,41 +233,48 @@ class FileExport
*/
private function processDocument(array $filesData, object $document): array
{
if (
$document->file_type === 'file' &&
isset($this->course->used_page_doc_ids) &&
in_array($document->source_id, $this->course->used_page_doc_ids)
) {
if ($document->file_type !== 'file') {
return $filesData;
}
if (
$document->file_type === 'file' &&
pathinfo($document->path, PATHINFO_EXTENSION) === 'html' &&
substr_count($document->path, '/') === 1
) {
return $filesData;
}
$fileData = $this->getFileData($document);
$filepath = $this->buildMoodleFilepathFromChamiloPath((string) $document->path);
if ($document->file_type === 'file') {
$extension = pathinfo($document->path, PATHINFO_EXTENSION);
if (!in_array(strtolower($extension), ['html', 'htm'])) {
$fileData = $this->getFileData($document);
$fileData['filepath'] = '/Documents/';
$fileData['contextid'] = 0;
$fileData['component'] = 'mod_folder';
$filesData['files'][] = $fileData;
}
} elseif ($document->file_type === 'folder') {
$folderFiles = \DocumentManager::getAllDocumentsByParentId($this->course->info, $document->source_id);
foreach ($folderFiles as $file) {
$filesData['files'][] = $this->getFolderFileData($file, (int) $document->source_id, '/Documents/'.dirname($file['path']).'/');
}
}
$fileData['filepath'] = $filepath;
$fileData['contextid'] = ActivityExport::DOCS_MODULE_ID;
$fileData['component'] = 'mod_folder';
$fileData['filearea'] = 'content';
$fileData['itemid'] = ActivityExport::DOCS_MODULE_ID;
$filesData['files'][] = $fileData;
return $filesData;
}
/**
* Build a Moodle filepath from a Chamilo document path.
*/
private function buildMoodleFilepathFromChamiloPath(string $documentPath): string
{
$normalizedPath = $this->stripChamiloDocumentPrefix($documentPath);
$normalizedPath = ltrim(str_replace('\\', '/', $normalizedPath), '/');
$relDir = dirname($normalizedPath);
return $this->ensureTrailingSlash($relDir === '.' ? '/' : '/'.$relDir.'/');
}
/**
* Remove the internal Chamilo document prefix from a path.
*/
private function stripChamiloDocumentPrefix(string $path): string
{
$path = str_replace('\\', '/', trim($path));
$path = preg_replace('#^/?document/#', '', $path);
return $path;
}
/**
* Get file data for a single document.
*/
@@ -253,40 +307,6 @@ class FileExport
];
}
/**
* Get file data for files inside a folder.
*/
private function getFolderFileData(array $file, int $sourceId, string $parentPath = '/Documents/'): array
{
$adminData = MoodleExport::getAdminUserData();
$adminId = $adminData['id'];
$contenthash = hash('sha1', basename($file['path']));
$mimetype = $this->getMimeType($file['path']);
$filename = basename($file['path']);
$filepath = $this->ensureTrailingSlash($parentPath);
return [
'id' => $file['id'],
'contenthash' => $contenthash,
'contextid' => $sourceId,
'component' => 'mod_folder',
'filearea' => 'content',
'itemid' => (int) $file['id'],
'filepath' => $filepath,
'documentpath' => 'document/'.$file['path'],
'filename' => $filename,
'userid' => $adminId,
'filesize' => $file['size'],
'mimetype' => $mimetype,
'status' => 0,
'timecreated' => time() - 3600,
'timemodified' => time(),
'source' => $file['title'],
'author' => 'Unknown',
'license' => 'allrightsreserved',
];
}
/**
* Ensure the directory path has a trailing slash.
*/
@@ -301,17 +321,6 @@ class FileExport
return rtrim($path, '/').'/';
}
/**
* Get MIME type based on the file extension.
*/
public function getMimeType($filePath): string
{
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
$mimeTypes = $this->getMimeTypes();
return $mimeTypes[$extension] ?? 'application/octet-stream';
}
/**
* Get an array of file extensions and their corresponding MIME types.
*/
@@ -323,8 +332,17 @@ class FileExport
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'svg' => 'image/svg+xml',
'webp' => 'image/webp',
'html' => 'text/html',
'htm' => 'text/html',
'txt' => 'text/plain',
'css' => 'text/css',
'js' => 'application/javascript',
'mp4' => 'video/mp4',
'webm' => 'video/webm',
'mp3' => 'audio/mpeg',
'ogg' => 'audio/ogg',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls' => 'application/vnd.ms-excel',
+6 -13
View File
@@ -44,12 +44,12 @@ class FolderExport extends ActivityExport
*/
public function getData(int $folderId, int $sectionId): ?array
{
if ($folderId === 0) {
if ($folderId === 0 || $folderId === ActivityExport::DOCS_MODULE_ID) {
return [
'id' => 0,
'moduleid' => 0,
'id' => ActivityExport::DOCS_MODULE_ID,
'moduleid' => ActivityExport::DOCS_MODULE_ID,
'modulename' => 'folder',
'contextid' => 0,
'contextid' => ActivityExport::DOCS_MODULE_ID,
'name' => 'Documents',
'sectionid' => $sectionId,
'timemodified' => time(),
@@ -98,17 +98,10 @@ class FolderExport extends ActivityExport
private function getFilesForFolder(int $folderId): array
{
$files = [];
if ($folderId === 0) {
if ($folderId === ActivityExport::DOCS_MODULE_ID) {
foreach ($this->course->resources[RESOURCE_DOCUMENT] as $doc) {
if ($doc->file_type === 'file') {
$files[] = [
'id' => (int) $doc->source_id,
'contenthash' => hash('sha1', basename($doc->path)),
'filename' => basename($doc->path),
'filepath' => '/Documents/',
'filesize' => (int) $doc->size,
'mimetype' => $this->getMimeType($doc->path),
];
$files[] = ['id' => (int) $doc->source_id];
}
}
}
+149 -59
View File
@@ -11,6 +11,8 @@ namespace moodleexport;
*/
class ForumExport extends ActivityExport
{
private static int $embeddedFileGlobalSeq = 0;
/**
* Export all forum data into a single Moodle forum activity.
*
@@ -21,13 +23,18 @@ class ForumExport extends ActivityExport
*/
public function export($activityId, $exportDir, $moduleId, $sectionId): void
{
// Prepare the directory where the forum export will be saved
$forumDir = $this->prepareActivityDirectory($exportDir, 'forum', $moduleId);
$effectiveModuleId = (int) $moduleId;
if ($effectiveModuleId <= 0) {
$effectiveModuleId = (int) $activityId;
}
// Retrieve forum data
$forumData = $this->getData($activityId, $sectionId);
$forumDir = $this->prepareActivityDirectory($exportDir, 'forum', $effectiveModuleId);
$forumData = $this->getData((int) $activityId, (int) $sectionId, $effectiveModuleId);
if (empty($forumData)) {
return;
}
// Generate XML files for the forum
$this->createForumXml($forumData, $forumDir);
$this->createModuleXml($forumData, $forumDir);
$this->createGradesXml($forumData, $forumDir);
@@ -43,50 +50,123 @@ class ForumExport extends ActivityExport
/**
* Get all forum data from the course.
*/
public function getData(int $forumId, int $sectionId): ?array
public function getData(int $forumId, int $sectionId, ?int $moduleId = null): ?array
{
$forum = $this->course->resources['forum'][$forumId]->obj;
$adminData = MoodleExport::getAdminUserData();
$adminId = $adminData['id'];
$threads = [];
foreach ($this->course->resources['thread'] as $threadId => $thread) {
if ($thread->obj->forum_id == $forumId) {
// Get the posts for each thread
$posts = [];
foreach ($this->course->resources['post'] as $postId => $post) {
if ($post->obj->thread_id == $threadId) {
$posts[] = [
'id' => $post->obj->post_id,
'userid' => $adminId,
'message' => $post->obj->post_text,
'created' => strtotime($post->obj->post_date),
'modified' => strtotime($post->obj->post_date),
];
}
}
$threads[] = [
'id' => $thread->obj->thread_id,
'title' => $thread->obj->thread_title,
'userid' => $adminId,
'timemodified' => strtotime($thread->obj->thread_date),
'usermodified' => $adminId,
'posts' => $posts,
];
}
if (empty($this->course->resources[RESOURCE_FORUM][$forumId])) {
return null;
}
$fileIds = [];
$forum = $this->course->resources[RESOURCE_FORUM][$forumId]->obj;
$effectiveModuleId = (int) ($moduleId ?? $forumId);
if ($effectiveModuleId <= 0) {
$effectiveModuleId = $forumId;
}
$adminData = MoodleExport::getAdminUserData();
$adminId = (int) ($adminData['id'] ?? 1);
$name = (string) ($forum->forum_title ?? '');
if ($sectionId > 0) {
$name = $this->lpItemTitle($sectionId, RESOURCE_FORUM, $forumId, $name);
}
$name = $this->sanitizeMoodleActivityName($name, 255);
$descriptionResult = $this->extractEmbeddedFilesAndNormalizeContent(
(string) ($forum->forum_comment ?? ''),
$effectiveModuleId,
'mod_forum',
'intro',
0,
fn (int $sequence): int => $this->buildForumEmbeddedFileId()
);
$forumFiles = $descriptionResult['files'];
$threads = [];
$threadResources = $this->course->resources['thread'] ?? [];
$postResources = $this->course->resources['post'] ?? [];
foreach ($threadResources as $threadId => $thread) {
if ((int) ($thread->obj->forum_id ?? 0) !== $forumId) {
continue;
}
$threadPosts = [];
foreach ($postResources as $post) {
if ((int) ($post->obj->thread_id ?? 0) !== (int) $threadId) {
continue;
}
$postId = (int) ($post->obj->post_id ?? 0);
$postDate = strtotime((string) ($post->obj->post_date ?? 'now'));
if ($postDate === false) {
$postDate = time();
}
$messageResult = $this->extractEmbeddedFilesAndNormalizeContent(
(string) ($post->obj->post_text ?? ''),
$effectiveModuleId,
'mod_forum',
'post',
$postId,
fn (int $sequence): int => $this->buildForumEmbeddedFileId()
);
if (!empty($messageResult['files'])) {
$forumFiles = array_merge($forumFiles, $messageResult['files']);
}
$threadPosts[] = [
'id' => $postId,
'userid' => $adminId,
'message' => $messageResult['content'],
'created' => $postDate,
'modified' => $postDate,
];
}
usort($threadPosts, static function (array $a, array $b): int {
return ((int) $a['created']) <=> ((int) $b['created']);
});
$firstPostId = 0;
foreach ($threadPosts as $index => &$postData) {
if ($index === 0) {
$firstPostId = (int) $postData['id'];
$postData['parent'] = 0;
} else {
$postData['parent'] = $firstPostId;
}
$postData['mailed'] = 0;
$postData['subject'] = (string) ($thread->obj->thread_title ?? get_lang('Forum'));
}
unset($postData);
$threadDate = strtotime((string) ($thread->obj->thread_date ?? 'now'));
if ($threadDate === false) {
$threadDate = time();
}
$threads[] = [
'id' => (int) ($thread->obj->thread_id ?? 0),
'title' => (string) ($thread->obj->thread_title ?? ''),
'firstpost' => $firstPostId,
'userid' => $adminId,
'timemodified' => $threadDate,
'usermodified' => $adminId,
'posts' => $threadPosts,
];
}
return [
'id' => $forumId,
'moduleid' => $forumId,
'moduleid' => $effectiveModuleId,
'modulename' => 'forum',
'contextid' => $this->course->info['real_id'],
'name' => $forum->forum_title,
'description' => $forum->forum_comment,
'contextid' => $effectiveModuleId,
'name' => $name,
'description' => $descriptionResult['content'],
'timecreated' => time(),
'timemodified' => time(),
'sectionid' => $sectionId,
@@ -94,7 +174,7 @@ class ForumExport extends ActivityExport
'userid' => $adminId,
'threads' => $threads,
'users' => [$adminId],
'files' => $fileIds,
'files' => $forumFiles,
];
}
@@ -107,8 +187,8 @@ class ForumExport extends ActivityExport
$xmlContent .= '<activity id="'.$forumData['id'].'" moduleid="'.$forumData['moduleid'].'" modulename="'.$forumData['modulename'].'" contextid="'.$forumData['contextid'].'">'.PHP_EOL;
$xmlContent .= ' <forum id="'.$forumData['id'].'">'.PHP_EOL;
$xmlContent .= ' <type>general</type>'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars($forumData['name']).'</name>'.PHP_EOL;
$xmlContent .= ' <intro>'.htmlspecialchars($forumData['description']).'</intro>'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars((string) $forumData['name']).'</name>'.PHP_EOL;
$xmlContent .= ' <intro><![CDATA['.(string) $forumData['description'].']]></intro>'.PHP_EOL;
$xmlContent .= ' <introformat>1</introformat>'.PHP_EOL;
$xmlContent .= ' <duedate>0</duedate>'.PHP_EOL;
$xmlContent .= ' <cutoffdate>0</cutoffdate>'.PHP_EOL;
@@ -132,13 +212,12 @@ class ForumExport extends ActivityExport
$xmlContent .= ' <displaywordcount>0</displaywordcount>'.PHP_EOL;
$xmlContent .= ' <lockdiscussionafter>0</lockdiscussionafter>'.PHP_EOL;
$xmlContent .= ' <grade_forum>0</grade_forum>'.PHP_EOL;
// Add forum threads
$xmlContent .= ' <discussions>'.PHP_EOL;
foreach ($forumData['threads'] as $thread) {
$xmlContent .= ' <discussion id="'.$thread['id'].'">'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars($thread['title']).'</name>'.PHP_EOL;
$xmlContent .= ' <firstpost>'.$thread['firstpost'].'</firstpost>'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars((string) $thread['title']).'</name>'.PHP_EOL;
$xmlContent .= ' <firstpost>'.(int) ($thread['firstpost'] ?? 0).'</firstpost>'.PHP_EOL;
$xmlContent .= ' <userid>'.$thread['userid'].'</userid>'.PHP_EOL;
$xmlContent .= ' <groupid>-1</groupid>'.PHP_EOL;
$xmlContent .= ' <assessed>0</assessed>'.PHP_EOL;
@@ -148,18 +227,17 @@ class ForumExport extends ActivityExport
$xmlContent .= ' <timeend>0</timeend>'.PHP_EOL;
$xmlContent .= ' <pinned>0</pinned>'.PHP_EOL;
$xmlContent .= ' <timelocked>0</timelocked>'.PHP_EOL;
// Add forum posts to the thread
$xmlContent .= ' <posts>'.PHP_EOL;
foreach ($thread['posts'] as $post) {
$xmlContent .= ' <post id="'.$post['id'].'">'.PHP_EOL;
$xmlContent .= ' <parent>'.$post['parent'].'</parent>'.PHP_EOL;
$xmlContent .= ' <parent>'.(int) ($post['parent'] ?? 0).'</parent>'.PHP_EOL;
$xmlContent .= ' <userid>'.$post['userid'].'</userid>'.PHP_EOL;
$xmlContent .= ' <created>'.$post['created'].'</created>'.PHP_EOL;
$xmlContent .= ' <modified>'.$post['modified'].'</modified>'.PHP_EOL;
$xmlContent .= ' <mailed>'.$post['mailed'].'</mailed>'.PHP_EOL;
$xmlContent .= ' <subject>'.htmlspecialchars($post['subject']).'</subject>'.PHP_EOL;
$xmlContent .= ' <message>'.htmlspecialchars($post['message']).'</message>'.PHP_EOL;
$xmlContent .= ' <mailed>'.(int) ($post['mailed'] ?? 0).'</mailed>'.PHP_EOL;
$xmlContent .= ' <subject>'.htmlspecialchars((string) ($post['subject'] ?? '')).'</subject>'.PHP_EOL;
$xmlContent .= ' <message><![CDATA['.(string) ($post['message'] ?? '').']]></message>'.PHP_EOL;
$xmlContent .= ' <messageformat>1</messageformat>'.PHP_EOL;
$xmlContent .= ' <messagetrust>0</messagetrust>'.PHP_EOL;
$xmlContent .= ' <attachment></attachment>'.PHP_EOL;
@@ -170,19 +248,31 @@ class ForumExport extends ActivityExport
$xmlContent .= ' </ratings>'.PHP_EOL;
$xmlContent .= ' </post>'.PHP_EOL;
}
$xmlContent .= ' </posts>'.PHP_EOL;
$xmlContent .= ' <discussion_subs>'.PHP_EOL;
$xmlContent .= ' <discussion_sub id="'.$thread['id'].'">'.PHP_EOL;
$xmlContent .= ' <userid>'.$thread['userid'].'</userid>'.PHP_EOL;
$xmlContent .= ' <preference>'.$thread['timemodified'].'</preference>'.PHP_EOL;
$xmlContent .= ' </discussion_sub>'.PHP_EOL;
$xmlContent .= ' <discussion_sub id="'.$thread['id'].'">'.PHP_EOL;
$xmlContent .= ' <userid>'.$thread['userid'].'</userid>'.PHP_EOL;
$xmlContent .= ' <preference>'.$thread['timemodified'].'</preference>'.PHP_EOL;
$xmlContent .= ' </discussion_sub>'.PHP_EOL;
$xmlContent .= ' </discussion_subs>'.PHP_EOL;
$xmlContent .= ' </discussion>'.PHP_EOL;
}
$xmlContent .= ' </discussions>'.PHP_EOL;
$xmlContent .= ' </forum>'.PHP_EOL;
$xmlContent .= '</activity>';
$this->createXmlFile('forum', $xmlContent, $forumDir);
}
/**
* Build a stable embedded file id for forum files.
*/
private function buildForumEmbeddedFileId(): int
{
self::$embeddedFileGlobalSeq++;
return 1400000000 + self::$embeddedFileGlobalSeq;
}
}
+63 -19
View File
@@ -11,6 +11,8 @@ namespace moodleexport;
*/
class GlossaryExport extends ActivityExport
{
private static int $embeddedFileGlobalSeq = 0;
/**
* Export all glossary terms into a single Moodle glossary.
*
@@ -21,13 +23,18 @@ class GlossaryExport extends ActivityExport
*/
public function export($activityId, $exportDir, $moduleId, $sectionId): void
{
// Prepare the directory where the glossary export will be saved
$glossaryDir = $this->prepareActivityDirectory($exportDir, 'glossary', $moduleId);
$effectiveModuleId = (int) $moduleId;
if ($effectiveModuleId <= 0) {
$effectiveModuleId = (int) $activityId;
}
// Retrieve glossary data
$glossaryData = $this->getData($activityId, $sectionId);
$glossaryDir = $this->prepareActivityDirectory($exportDir, 'glossary', $effectiveModuleId);
$glossaryData = $this->getData((int) $activityId, (int) $sectionId, $effectiveModuleId);
if (empty($glossaryData)) {
return;
}
// Generate XML files for the glossary
$this->createGlossaryXml($glossaryData, $glossaryDir);
$this->createModuleXml($glossaryData, $glossaryDir);
$this->createGradesXml($glossaryData, $glossaryDir);
@@ -43,30 +50,57 @@ class GlossaryExport extends ActivityExport
/**
* Get all terms from the course and group them into a single glossary.
*/
public function getData(int $glossaryId, int $sectionId): ?array
public function getData(int $glossaryId, int $sectionId, ?int $moduleId = null): ?array
{
if (empty($this->course->resources['glossary'])) {
return null;
}
$adminData = MoodleExport::getAdminUserData();
$adminId = $adminData['id'];
$adminId = (int) ($adminData['id'] ?? 1);
$effectiveModuleId = (int) ($moduleId ?? $glossaryId);
if ($effectiveModuleId <= 0) {
$effectiveModuleId = $glossaryId;
}
$glossaryEntries = [];
$glossaryFiles = [];
foreach ($this->course->resources['glossary'] as $glossary) {
$entryId = (int) ($glossary->glossary_id ?? 0);
$definitionResult = $this->extractEmbeddedFilesAndNormalizeContent(
(string) ($glossary->description ?? ''),
$effectiveModuleId,
'mod_glossary',
'entry',
$entryId,
fn (int $sequence): int => $this->buildGlossaryEmbeddedFileId()
);
if (!empty($definitionResult['files'])) {
$glossaryFiles = array_merge($glossaryFiles, $definitionResult['files']);
}
$glossaryEntries[] = [
'id' => $glossary->glossary_id,
'id' => $entryId,
'userid' => $adminId,
'concept' => $glossary->name,
'definition' => $glossary->description,
'concept' => $this->sanitizeMoodleActivityName((string) ($glossary->name ?? ''), 255),
'definition' => $definitionResult['content'],
'timecreated' => time(),
'timemodified' => time(),
];
}
// Return the glossary data with all terms included
$glossaryName = $this->sanitizeMoodleActivityName((string) get_lang('Glossary'), 255);
return [
'id' => $glossaryId,
'moduleid' => $glossaryId,
'moduleid' => $effectiveModuleId,
'modulename' => 'glossary',
'contextid' => $this->course->info['real_id'],
'name' => get_lang('Glossary'),
'contextid' => $effectiveModuleId,
'name' => $glossaryName,
'description' => '',
'timecreated' => time(),
'timemodified' => time(),
@@ -75,7 +109,7 @@ class GlossaryExport extends ActivityExport
'userid' => $adminId,
'entries' => $glossaryEntries,
'users' => [$adminId],
'files' => [],
'files' => $glossaryFiles,
];
}
@@ -87,7 +121,7 @@ class GlossaryExport extends ActivityExport
$xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
$xmlContent .= '<activity id="'.$glossaryData['id'].'" moduleid="'.$glossaryData['moduleid'].'" modulename="'.$glossaryData['modulename'].'" contextid="'.$glossaryData['contextid'].'">'.PHP_EOL;
$xmlContent .= ' <glossary id="'.$glossaryData['id'].'">'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars($glossaryData['name']).'</name>'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars((string) $glossaryData['name']).'</name>'.PHP_EOL;
$xmlContent .= ' <intro></intro>'.PHP_EOL;
$xmlContent .= ' <introformat>1</introformat>'.PHP_EOL;
$xmlContent .= ' <allowduplicatedentries>0</allowduplicatedentries>'.PHP_EOL;
@@ -114,12 +148,11 @@ class GlossaryExport extends ActivityExport
$xmlContent .= ' <completionentries>0</completionentries>'.PHP_EOL;
$xmlContent .= ' <entries>'.PHP_EOL;
// Add glossary terms (entries)
foreach ($glossaryData['entries'] as $entry) {
$xmlContent .= ' <entry id="'.$entry['id'].'">'.PHP_EOL;
$xmlContent .= ' <userid>'.$entry['userid'].'</userid>'.PHP_EOL;
$xmlContent .= ' <concept>'.htmlspecialchars($entry['concept']).'</concept>'.PHP_EOL;
$xmlContent .= ' <definition><![CDATA['.$entry['definition'].']]></definition>'.PHP_EOL;
$xmlContent .= ' <concept>'.htmlspecialchars((string) $entry['concept']).'</concept>'.PHP_EOL;
$xmlContent .= ' <definition><![CDATA['.(string) $entry['definition'].']]></definition>'.PHP_EOL;
$xmlContent .= ' <definitionformat>1</definitionformat>'.PHP_EOL;
$xmlContent .= ' <definitiontrust>0</definitiontrust>'.PHP_EOL;
$xmlContent .= ' <attachment></attachment>'.PHP_EOL;
@@ -135,6 +168,7 @@ class GlossaryExport extends ActivityExport
$xmlContent .= ' </ratings>'.PHP_EOL;
$xmlContent .= ' </entry>'.PHP_EOL;
}
$xmlContent .= ' </entries>'.PHP_EOL;
$xmlContent .= ' <entriestags></entriestags>'.PHP_EOL;
$xmlContent .= ' <categories></categories>'.PHP_EOL;
@@ -143,4 +177,14 @@ class GlossaryExport extends ActivityExport
$this->createXmlFile('glossary', $xmlContent, $glossaryDir);
}
/**
* Build a stable embedded file id for glossary files.
*/
private function buildGlossaryEmbeddedFileId(): int
{
self::$embeddedFileGlobalSeq++;
return 1500000000 + self::$embeddedFileGlobalSeq;
}
}
File diff suppressed because it is too large Load Diff
+110 -104
View File
@@ -11,6 +11,9 @@ namespace moodleexport;
*/
class PageExport extends ActivityExport
{
public const INTRO_PAGE_MODULE_ID = 910000000;
private const CONTENT_REVISION = 1;
/**
* Export a page to the specified directory.
*
@@ -21,13 +24,21 @@ class PageExport extends ActivityExport
*/
public function export($activityId, $exportDir, $moduleId, $sectionId): void
{
// Prepare the directory where the page export will be saved
$pageDir = $this->prepareActivityDirectory($exportDir, 'page', $moduleId);
$effectiveModuleId = (int) $moduleId;
if ($effectiveModuleId <= 0 && (int) $activityId === 0) {
$effectiveModuleId = self::INTRO_PAGE_MODULE_ID;
}
if ($effectiveModuleId <= 0) {
$effectiveModuleId = (int) $activityId;
}
// Retrieve page data
$pageData = $this->getData($activityId, $sectionId);
$pageDir = $this->prepareActivityDirectory($exportDir, 'page', $effectiveModuleId);
$pageData = $this->getData((int) $activityId, (int) $sectionId, $effectiveModuleId);
if (empty($pageData)) {
return;
}
// Generate XML files
$this->createPageXml($pageData, $pageDir);
$this->createModuleXml($pageData, $pageDir);
$this->createGradesXml($pageData, $pageDir);
@@ -42,91 +53,87 @@ class PageExport extends ActivityExport
/**
* Get page data dynamically from the course.
*/
public function getData(int $pageId, int $sectionId): ?array
public function getData(int $pageId, int $sectionId, ?int $moduleId = null): ?array
{
$contextid = $this->course->info['real_id'];
if ($pageId === 0) {
$introText = trim($this->course->resources[RESOURCE_TOOL_INTRO]['course_homepage']->intro_text ?? '');
if (!empty($introText)) {
$files = [];
$resources = \DocumentManager::get_resources_from_source_html($introText);
$courseInfo = api_get_course_info($this->course->code);
$adminId = MoodleExport::getAdminUserData()['id'];
foreach ($resources as [$src]) {
if (preg_match('#/document(/[^"\']+)#', $src, $matches)) {
$path = $matches[1];
$docId = \DocumentManager::get_document_id($courseInfo, $path);
if ($docId) {
$this->course->used_page_doc_ids[] = $docId;
$document = \DocumentManager::get_document_data_by_id($docId, $this->course->code);
if ($document) {
$contenthash = hash('sha1', basename($document['path']));
$mimetype = (new FileExport($this->course))->getMimeType($document['path']);
$files[] = [
'id' => $document['id'],
'contenthash' => $contenthash,
'contextid' => $contextid,
'component' => 'mod_page',
'filearea' => 'content',
'itemid' => 1,
'filepath' => '/Documents/',
'documentpath' => 'document' . $document['path'],
'filename' => basename($document['path']),
'userid' => $adminId,
'filesize' => $document['size'],
'mimetype' => $mimetype,
'status' => 0,
'timecreated' => time() - 3600,
'timemodified' => time(),
'source' => $document['title'],
'author' => 'Unknown',
'license' => 'allrightsreserved',
];
}
}
}
}
return [
'id' => 0,
'moduleid' => 0,
'modulename' => 'page',
'contextid' => $contextid,
'name' => get_lang('Introduction'),
'intro' => '',
'content' => $this->normalizeContent($introText),
'sectionid' => $sectionId,
'sectionnumber' => 1,
'display' => 0,
'timemodified' => time(),
'users' => [],
'files' => $files,
];
$introText = trim((string) ($this->course->resources[RESOURCE_TOOL_INTRO]['course_homepage']->intro_text ?? ''));
if ($introText === '') {
return null;
}
$effectiveModuleId = (int) ($moduleId ?? self::INTRO_PAGE_MODULE_ID);
if ($effectiveModuleId <= 0) {
$effectiveModuleId = self::INTRO_PAGE_MODULE_ID;
}
$introResult = $this->extractEmbeddedFilesAndNormalizeContent(
$introText,
$effectiveModuleId,
'mod_page',
'content',
0,
fn (int $sequence): int => $this->buildPageFileId($effectiveModuleId, $effectiveModuleId, $sequence, true)
);
return [
'id' => $effectiveModuleId,
'moduleid' => $effectiveModuleId,
'modulename' => 'page',
'contextid' => $effectiveModuleId,
'name' => $this->sanitizeMoodleActivityName((string) get_lang('Introduction'), 255),
'intro' => '',
'content' => $introResult['content'],
'sectionid' => $sectionId,
'sectionnumber' => 1,
'display' => 5,
'timemodified' => time(),
'users' => [],
'files' => $introResult['files'],
];
}
$pageResources = $this->course->resources[RESOURCE_DOCUMENT] ?? [];
foreach ($pageResources as $page) {
if ($page->source_id == $pageId) {
return [
'id' => $page->source_id,
'moduleid' => $page->source_id,
'modulename' => 'page',
'contextid' => $contextid,
'name' => $page->title,
'intro' => $page->comment ?? '',
'content' => $this->normalizeContent($this->getPageContent($page)),
'sectionid' => $sectionId,
'sectionnumber' => 1,
'display' => 0,
'timemodified' => time(),
'users' => [],
'files' => [],
];
if ((int) $page->source_id !== $pageId) {
continue;
}
$effectiveModuleId = (int) ($moduleId ?? $page->source_id);
if ($effectiveModuleId <= 0) {
$effectiveModuleId = (int) $page->source_id;
}
$pageName = (string) ($page->title ?? '');
if ($sectionId > 0) {
$pageName = $this->lpItemTitle($sectionId, RESOURCE_DOCUMENT, $pageId, $pageName);
}
$pageName = $this->sanitizeMoodleActivityName($pageName, 255);
$rawContent = $this->getPageContent($page);
$pageResult = $this->extractEmbeddedFilesAndNormalizeContent(
$rawContent,
$effectiveModuleId,
'mod_page',
'content',
0,
fn (int $sequence): int => $this->buildPageFileId($effectiveModuleId, $effectiveModuleId, $sequence, false)
);
return [
'id' => (int) $page->source_id,
'moduleid' => $effectiveModuleId,
'modulename' => 'page',
'contextid' => $effectiveModuleId,
'name' => $pageName,
'intro' => (string) ($page->comment ?? ''),
'content' => $pageResult['content'],
'sectionid' => $sectionId,
'sectionnumber' => 1,
'display' => 5,
'timemodified' => time(),
'users' => [],
'files' => $pageResult['files'],
];
}
return null;
@@ -140,15 +147,15 @@ class PageExport extends ActivityExport
$xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
$xmlContent .= '<activity id="'.$pageData['id'].'" moduleid="'.$pageData['moduleid'].'" modulename="page" contextid="'.$pageData['contextid'].'">'.PHP_EOL;
$xmlContent .= ' <page id="'.$pageData['id'].'">'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars($pageData['name']).'</name>'.PHP_EOL;
$xmlContent .= ' <intro>'.htmlspecialchars($pageData['intro']).'</intro>'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars((string) $pageData['name']).'</name>'.PHP_EOL;
$xmlContent .= ' <intro><![CDATA['.(string) $pageData['intro'].']]></intro>'.PHP_EOL;
$xmlContent .= ' <introformat>1</introformat>'.PHP_EOL;
$xmlContent .= ' <content>'.htmlspecialchars($pageData['content']).'</content>'.PHP_EOL;
$xmlContent .= ' <content><![CDATA['.(string) $pageData['content'].']]></content>'.PHP_EOL;
$xmlContent .= ' <contentformat>1</contentformat>'.PHP_EOL;
$xmlContent .= ' <legacyfiles>0</legacyfiles>'.PHP_EOL;
$xmlContent .= ' <display>5</display>'.PHP_EOL;
$xmlContent .= ' <display>'.(int) ($pageData['display'] ?? 5).'</display>'.PHP_EOL;
$xmlContent .= ' <displayoptions>a:3:{s:12:"printheading";s:1:"1";s:10:"printintro";s:1:"0";s:17:"printlastmodified";s:1:"1";}</displayoptions>'.PHP_EOL;
$xmlContent .= ' <revision>1</revision>'.PHP_EOL;
$xmlContent .= ' <revision>'.self::CONTENT_REVISION.'</revision>'.PHP_EOL;
$xmlContent .= ' <timemodified>'.$pageData['timemodified'].'</timemodified>'.PHP_EOL;
$xmlContent .= ' </page>'.PHP_EOL;
$xmlContent .= '</activity>';
@@ -156,22 +163,16 @@ class PageExport extends ActivityExport
$this->createXmlFile('page', $xmlContent, $pageDir);
}
private function normalizeContent(string $html): string
/**
* Build a unique files.xml id for page embedded files.
*/
private function buildPageFileId(int $moduleId, int $contextId, int $sequence, bool $isIntro): int
{
return preg_replace_callback(
'#<img[^>]+src=["\'](?<url>[^"\']+)["\']#i',
function ($match) {
$src = $match['url'];
if ($isIntro) {
return 1150000000 + max(0, $contextId) + max(1, $sequence);
}
if (preg_match('#/courses/[^/]+/document/(.+)$#', $src, $parts)) {
$filename = basename($parts[1]);
return str_replace($src, '@@PLUGINFILE@@/Documents/' . $filename, $match[0]);
}
return $match[0];
},
$html
);
return 1100000000 + max(0, $moduleId) + max(1, $sequence);
}
/**
@@ -179,10 +180,15 @@ class PageExport extends ActivityExport
*/
private function getPageContent(object $page): string
{
if ($page->file_type === 'file') {
return file_get_contents($this->course->path.$page->path);
if (($page->file_type ?? '') !== 'file') {
return '';
}
return '';
$absolutePath = $this->course->path.$page->path;
if (!is_file($absolutePath)) {
return '';
}
return (string) file_get_contents($absolutePath);
}
}
+493 -147
View File
@@ -14,6 +14,8 @@ use FillBlanks;
*/
class QuizExport extends ActivityExport
{
private static $embeddedFileGlobalSeq = 0;
/**
* Export a quiz to the specified directory.
*
@@ -24,13 +26,13 @@ class QuizExport extends ActivityExport
*/
public function export($activityId, $exportDir, $moduleId, $sectionId): void
{
// Prepare the directory where the quiz export will be saved
$quizDir = $this->prepareActivityDirectory($exportDir, 'quiz', $moduleId);
$quizDir = $this->prepareActivityDirectory($exportDir, 'quiz', (int) $moduleId);
$quizData = $this->getData((int) $activityId, (int) $sectionId, (int) $moduleId);
// Retrieve quiz data
$quizData = $this->getData($activityId, $sectionId);
if (empty($quizData)) {
return;
}
// Generate XML files
$this->createQuizXml($quizData, $quizDir);
$this->createModuleXml($quizData, $quizDir);
$this->createGradesXml($quizData, $quizDir);
@@ -44,71 +46,188 @@ class QuizExport extends ActivityExport
$this->createCalendarXml($quizData, $quizDir);
}
/**
* Moodle requires a valid question behaviour.
*/
private function sanitizePreferredBehaviour($raw): string
{
$behaviour = trim((string) $raw);
$allowed = [
'deferredfeedback',
'adaptive',
'adaptivenopenalty',
'immediatefeedback',
'interactive',
'manualgraded',
'deferredcbm',
'immediatecbm',
'interactivecountback',
];
if ($behaviour === '' || !in_array($behaviour, $allowed, true)) {
return 'deferredfeedback';
}
return $behaviour;
}
/**
* Normalize an integer-like value.
*/
private function sanitizeInt($raw, int $default): int
{
if ($raw === null) {
return $default;
}
if (is_string($raw) && trim($raw) === '') {
return $default;
}
return is_numeric($raw) ? (int) $raw : $default;
}
/**
* Parse a float or return null when missing.
*/
private function parseNullableFloat($raw): ?float
{
if ($raw === null) {
return null;
}
if (is_string($raw) && trim($raw) === '') {
return null;
}
return is_numeric($raw) ? (float) $raw : null;
}
/**
* Moodle quiz navmethod accepted values.
*/
private function sanitizeNavMethod($raw): string
{
$v = trim((string) $raw);
return in_array($v, ['free', 'sequential'], true) ? $v : 'free';
}
/**
* Build a stable question category id per exported quiz occurrence.
*/
private function buildQuizQuestionCategoryId(int $moduleId): int
{
return 1000000000 + max(1, $moduleId);
}
/**
* Retrieves the quiz data.
*/
public function getData(int $quizId, int $sectionId): array
public function getData(int $quizId, int $sectionId, ?int $moduleId = null): array
{
$quizResources = $this->course->resources[RESOURCE_QUIZ];
$quizResources = $this->course->resources[RESOURCE_QUIZ] ?? [];
foreach ($quizResources as $quiz) {
if ($quiz->obj->iid == -1) {
continue;
}
if ($quiz->obj->iid == $quizId) {
$contextid = $quiz->obj->c_id;
return [
'id' => $quiz->obj->iid,
'name' => $quiz->obj->title,
'intro' => $quiz->obj->description,
'timeopen' => $quiz->obj->start_time ?? 0,
'timeclose' => $quiz->obj->end_time ?? 0,
'timelimit' => $quiz->obj->timelimit ?? 0,
'grademethod' => $quiz->obj->grademethod ?? 1,
'decimalpoints' => $quiz->obj->decimalpoints ?? 2,
'sumgrades' => $quiz->obj->sumgrades ?? 0,
'grade' => $quiz->obj->grade ?? 0,
'questionsperpage' => $quiz->obj->questionsperpage ?? 1,
'preferredbehaviour' => $quiz->obj->preferredbehaviour ?? 'deferredfeedback',
'shuffleanswers' => $quiz->obj->shuffleanswers ?? 1,
'questions' => $this->getQuestionsForQuiz($quizId),
'feedbacks' => $this->getFeedbacksForQuiz($quizId),
'sectionid' => $sectionId,
'moduleid' => $quiz->obj->iid ?? 0,
'modulename' => 'quiz',
'contextid' => $contextid,
'overduehandling' => $quiz->obj->overduehandling ?? 'autosubmit',
'graceperiod' => $quiz->obj->graceperiod ?? 0,
'canredoquestions' => $quiz->obj->canredoquestions ?? 0,
'attempts_number' => $quiz->obj->attempts_number ?? 0,
'attemptonlast' => $quiz->obj->attemptonlast ?? 0,
'questiondecimalpoints' => $quiz->obj->questiondecimalpoints ?? 2,
'reviewattempt' => $quiz->obj->reviewattempt ?? 0,
'reviewcorrectness' => $quiz->obj->reviewcorrectness ?? 0,
'reviewmarks' => $quiz->obj->reviewmarks ?? 0,
'reviewspecificfeedback' => $quiz->obj->reviewspecificfeedback ?? 0,
'reviewgeneralfeedback' => $quiz->obj->reviewgeneralfeedback ?? 0,
'reviewrightanswer' => $quiz->obj->reviewrightanswer ?? 0,
'reviewoverallfeedback' => $quiz->obj->reviewoverallfeedback ?? 0,
'timecreated' => $quiz->obj->insert_date ?? time(),
'timemodified' => $quiz->obj->lastedit_date ?? time(),
'password' => $quiz->obj->password ?? '',
'subnet' => $quiz->obj->subnet ?? '',
'browsersecurity' => $quiz->obj->browsersecurity ?? '-',
'delay1' => $quiz->obj->delay1 ?? 0,
'delay2' => $quiz->obj->delay2 ?? 0,
'showuserpicture' => $quiz->obj->showuserpicture ?? 0,
'showblocks' => $quiz->obj->showblocks ?? 0,
'completionattemptsexhausted' => $quiz->obj->completionattemptsexhausted ?? 0,
'completionpass' => $quiz->obj->completionpass ?? 0,
'completionminattempts' => $quiz->obj->completionminattempts ?? 0,
'allowofflineattempts' => $quiz->obj->allowofflineattempts ?? 0,
'users' => [],
'files' => [],
];
if ((int) $quiz->obj->iid !== $quizId) {
continue;
}
$courseCode = $this->course->info['code'] ?? ($this->course->code ?? '');
$courseInfo = api_get_course_info($courseCode);
$courseRealId = (int) ($courseInfo['real_id'] ?? 0);
$contextid = $this->buildQuestionBankContextId($courseRealId);
$effectiveModuleId = (int) ($moduleId ?? ($quiz->obj->iid ?? 0));
if ($effectiveModuleId <= 0) {
$effectiveModuleId = (int) ($quiz->obj->iid ?? 0);
}
$questionCategoryId = $this->buildQuizQuestionCategoryId($effectiveModuleId);
$name = (string) ($quiz->obj->title ?? '');
if ($sectionId > 0) {
$name = $this->lpItemTitle($sectionId, RESOURCE_QUIZ, $quizId, $name);
}
$name = $this->sanitizeMoodleActivityName($name, 255);
$adminData = MoodleExport::getAdminUserData();
$adminId = (int) ($adminData['id'] ?? 1);
$quizFiles = [];
$questions = $this->getQuestionsForQuiz(
$quizId,
$questionCategoryId,
$contextid,
$courseInfo,
$adminId,
$quizFiles
);
$computedSumgrades = 0.0;
foreach ($questions as $q) {
$computedSumgrades += (float) ($q['maxmark'] ?? 0.0);
}
$rawSumgrades = $this->parseNullableFloat($quiz->obj->sumgrades ?? null);
$rawGrade = $this->parseNullableFloat($quiz->obj->grade ?? null);
$sumgrades = $rawSumgrades !== null ? $rawSumgrades : $computedSumgrades;
$grade = $rawGrade !== null ? $rawGrade : ($sumgrades > 0 ? $sumgrades : 0.0);
return [
'id' => (int) ($quiz->obj->iid ?? 0),
'name' => $name,
'intro' => (string) ($quiz->obj->description ?? ''),
'timeopen' => $this->sanitizeInt($quiz->obj->start_time ?? null, 0),
'timeclose' => $this->sanitizeInt($quiz->obj->end_time ?? null, 0),
'timelimit' => $this->sanitizeInt($quiz->obj->timelimit ?? null, 0),
'grademethod' => $this->sanitizeInt($quiz->obj->grademethod ?? null, 1),
'decimalpoints' => $this->sanitizeInt($quiz->obj->decimalpoints ?? null, 2),
'sumgrades' => $sumgrades,
'grade' => $grade,
'questionsperpage' => $this->sanitizeInt($quiz->obj->questionsperpage ?? null, 1),
'preferredbehaviour' => $this->sanitizePreferredBehaviour($quiz->obj->preferredbehaviour ?? null),
'shuffleanswers' => $this->sanitizeInt($quiz->obj->shuffleanswers ?? null, 1),
'navmethod' => $this->sanitizeNavMethod($quiz->obj->navmethod ?? null),
'question_category_id' => $questionCategoryId,
'questions' => $questions,
'files' => $quizFiles,
'feedbacks' => $this->getFeedbacksForQuiz($quizId),
'sectionid' => $sectionId,
'moduleid' => $effectiveModuleId,
'modulename' => 'quiz',
'contextid' => $contextid,
'courseid' => $courseRealId,
'overduehandling' => (string) ($quiz->obj->overduehandling ?? 'autosubmit'),
'graceperiod' => $this->sanitizeInt($quiz->obj->graceperiod ?? null, 0),
'canredoquestions' => $this->sanitizeInt($quiz->obj->canredoquestions ?? null, 0),
'attempts_number' => $this->sanitizeInt($quiz->obj->attempts_number ?? null, 0),
'attemptonlast' => $this->sanitizeInt($quiz->obj->attemptonlast ?? null, 0),
'questiondecimalpoints' => $this->sanitizeInt($quiz->obj->questiondecimalpoints ?? null, 2),
'reviewattempt' => $this->sanitizeInt($quiz->obj->reviewattempt ?? null, 0),
'reviewcorrectness' => $this->sanitizeInt($quiz->obj->reviewcorrectness ?? null, 0),
'reviewmarks' => $this->sanitizeInt($quiz->obj->reviewmarks ?? null, 0),
'reviewspecificfeedback' => $this->sanitizeInt($quiz->obj->reviewspecificfeedback ?? null, 0),
'reviewgeneralfeedback' => $this->sanitizeInt($quiz->obj->reviewgeneralfeedback ?? null, 0),
'reviewrightanswer' => $this->sanitizeInt($quiz->obj->reviewrightanswer ?? null, 0),
'reviewoverallfeedback' => $this->sanitizeInt($quiz->obj->reviewoverallfeedback ?? null, 0),
'timecreated' => $this->sanitizeInt($quiz->obj->insert_date ?? null, time()),
'timemodified' => $this->sanitizeInt($quiz->obj->lastedit_date ?? null, time()),
'password' => (string) ($quiz->obj->password ?? ''),
'subnet' => (string) ($quiz->obj->subnet ?? ''),
'browsersecurity' => (string) ($quiz->obj->browsersecurity ?? '-'),
'delay1' => $this->sanitizeInt($quiz->obj->delay1 ?? null, 0),
'delay2' => $this->sanitizeInt($quiz->obj->delay2 ?? null, 0),
'showuserpicture' => $this->sanitizeInt($quiz->obj->showuserpicture ?? null, 0),
'showblocks' => $this->sanitizeInt($quiz->obj->showblocks ?? null, 0),
'completionattemptsexhausted' => $this->sanitizeInt($quiz->obj->completionattemptsexhausted ?? null, 0),
'completionpass' => $this->sanitizeInt($quiz->obj->completionpass ?? null, 0),
'completionminattempts' => $this->sanitizeInt($quiz->obj->completionminattempts ?? null, 0),
'allowofflineattempts' => $this->sanitizeInt($quiz->obj->allowofflineattempts ?? null, 0),
'users' => [],
];
}
return [];
@@ -119,16 +238,19 @@ class QuizExport extends ActivityExport
*/
public function exportQuestion(array $question): string
{
$questionText = (string) ($question['questiontext'] ?? 'No question text');
$questionName = $this->sanitizeQuestionName($questionText, 255);
$xmlContent = ' <question id="'.($question['id'] ?? '0').'">'.PHP_EOL;
$xmlContent .= ' <parent>0</parent>'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars($question['questiontext'] ?? 'No question text').'</name>'.PHP_EOL;
$xmlContent .= ' <questiontext>'.htmlspecialchars($question['questiontext'] ?? 'No question text').'</questiontext>'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars($questionName).'</name>'.PHP_EOL;
$xmlContent .= ' <questiontext>'.htmlspecialchars($questionText).'</questiontext>'.PHP_EOL;
$xmlContent .= ' <questiontextformat>1</questiontextformat>'.PHP_EOL;
$xmlContent .= ' <generalfeedback></generalfeedback>'.PHP_EOL;
$xmlContent .= ' <generalfeedbackformat>1</generalfeedbackformat>'.PHP_EOL;
$xmlContent .= ' <defaultmark>'.($question['maxmark'] ?? '0').'</defaultmark>'.PHP_EOL;
$xmlContent .= ' <penalty>0.3333333</penalty>'.PHP_EOL;
$xmlContent .= ' <qtype>'.htmlspecialchars(str_replace('_nosingle', '', $question['qtype']) ?? 'unknown').'</qtype>'.PHP_EOL;
$xmlContent .= ' <qtype>'.htmlspecialchars((string) str_replace('_nosingle', '', $question['qtype'] ?? 'unknown')).'</qtype>'.PHP_EOL;
$xmlContent .= ' <length>1</length>'.PHP_EOL;
$xmlContent .= ' <stamp>moodle+'.time().'+QUESTIONSTAMP</stamp>'.PHP_EOL;
$xmlContent .= ' <version>moodle+'.time().'+VERSIONSTAMP</version>'.PHP_EOL;
@@ -138,7 +260,6 @@ class QuizExport extends ActivityExport
$xmlContent .= ' <createdby>2</createdby>'.PHP_EOL;
$xmlContent .= ' <modifiedby>2</modifiedby>'.PHP_EOL;
// Add question type-specific content
switch ($question['qtype']) {
case 'multichoice':
$xmlContent .= $this->exportMultichoiceQuestion($question);
@@ -165,24 +286,58 @@ class QuizExport extends ActivityExport
/**
* Retrieves the questions for a specific quiz.
*/
private function getQuestionsForQuiz(int $quizId): array
{
private function getQuestionsForQuiz(
int $quizId,
int $questionCategoryId,
int $contextId,
array $courseInfo,
int $adminId,
array &$quizFiles
): array {
$questions = [];
$quizResources = $this->course->resources[RESOURCE_QUIZQUESTION] ?? [];
foreach ($quizResources as $questionId => $questionData) {
if (in_array($questionId, $this->course->resources[RESOURCE_QUIZ][$quizId]->obj->question_ids)) {
$categoryId = $questionData->question_category ?? 0;
$categoryId = $categoryId > 0 ? $categoryId : $this->getDefaultCategoryId();
$questions[] = [
'id' => $questionData->source_id,
'questiontext' => $questionData->question,
'qtype' => $this->mapQuestionType($questionData->quiz_type),
'questioncategoryid' => $categoryId,
'answers' => $this->getAnswersForQuestion($questionData->source_id),
'maxmark' => $questionData->ponderation ?? 1,
];
if (!in_array($questionId, $this->course->resources[RESOURCE_QUIZ][$quizId]->obj->question_ids)) {
continue;
}
$qRes = $this->extractQuestionEmbeddedFilesAndNormalizeContent(
(string) ($questionData->question ?? ''),
$contextId,
'questiontext',
(int) $questionData->source_id,
'',
$courseInfo,
$adminId
);
if (!empty($qRes['files'])) {
$quizFiles = array_merge($quizFiles, $qRes['files']);
}
$answers = $this->getAnswersForQuestion(
(int) $questionData->source_id,
$contextId,
$courseInfo,
$adminId,
$quizFiles
);
$mappedType = $this->mapQuestionType((string) $questionData->quiz_type);
if ($mappedType === 'unknown') {
continue;
}
$questions[] = [
'id' => (int) $questionData->source_id,
'questiontext' => $qRes['content'],
'qtype' => $mappedType,
'questioncategoryid' => $questionCategoryId,
'answers' => $answers,
'maxmark' => $questionData->ponderation ?? 1,
];
}
return $questions;
@@ -194,36 +349,69 @@ class QuizExport extends ActivityExport
private function mapQuestionType(string $quizType): string
{
switch ($quizType) {
case UNIQUE_ANSWER: return 'multichoice';
case MULTIPLE_ANSWER: return 'multichoice_nosingle';
case FILL_IN_BLANKS: return 'match';
case FREE_ANSWER: return 'shortanswer';
case CALCULATED_ANSWER: return 'calculated';
case UPLOAD_ANSWER: return 'fileupload';
default: return 'unknown';
case UNIQUE_ANSWER:
return 'multichoice';
case MULTIPLE_ANSWER:
return 'multichoice_nosingle';
case FILL_IN_BLANKS:
return 'match';
case FREE_ANSWER:
return 'shortanswer';
case CALCULATED_ANSWER:
return 'calculated';
case UPLOAD_ANSWER:
return 'fileupload';
default:
return 'unknown';
}
}
/**
* Retrieves the answers for a specific question ID.
*/
private function getAnswersForQuestion(int $questionId): array
{
static $globalCounter = 0;
private function getAnswersForQuestion(
int $questionId,
int $contextId,
array $courseInfo,
int $adminId,
array &$collectedFiles
): array {
$answers = [];
$quizResources = $this->course->resources[RESOURCE_QUIZQUESTION] ?? [];
foreach ($quizResources as $questionData) {
if ($questionData->source_id == $questionId) {
foreach ($questionData->answers as $answer) {
$globalCounter++;
$answers[] = [
'id' => $questionId * 1000 + $globalCounter,
'text' => $answer['answer'],
'fraction' => $answer['correct'] == '1' ? 100 : 0,
'feedback' => $answer['comment'],
];
if ((int) $questionData->source_id !== $questionId) {
continue;
}
$answerIndex = 0;
foreach ($questionData->answers as $answer) {
$answerIndex++;
$answerId = $this->buildStableQuestionAnswerId($questionId, $answerIndex);
$rawText = (string) ($answer['answer'] ?? '');
$res = $this->extractQuestionEmbeddedFilesAndNormalizeContent(
$rawText,
$contextId,
'answer',
$answerId,
'',
$courseInfo,
$adminId
);
if (!empty($res['files'])) {
$collectedFiles = array_merge($collectedFiles, $res['files']);
}
$answers[] = [
'id' => $answerId,
'text' => $res['content'],
'fraction' => (($answer['correct'] ?? '0') == '1') ? 100 : 0,
'feedback' => (string) ($answer['comment'] ?? ''),
];
}
}
@@ -239,9 +427,9 @@ class QuizExport extends ActivityExport
$quizResources = $this->course->resources[RESOURCE_QUIZ] ?? [];
foreach ($quizResources as $quiz) {
if ($quiz->obj->iid == $quizId) {
if ((int) $quiz->obj->iid === $quizId) {
$feedbacks[] = [
'feedbacktext' => $quiz->obj->description ?? '',
'feedbacktext' => (string) ($quiz->obj->description ?? ''),
'mingrade' => 0.00000,
'maxgrade' => $quiz->obj->grade ?? 10.00000,
];
@@ -251,36 +439,42 @@ class QuizExport extends ActivityExport
return $feedbacks;
}
private function getDefaultCategoryId(): int
{
return 1;
}
/**
* Creates the quiz.xml file.
*/
private function createQuizXml(array $quizData, string $destinationDir): void
{
$preferredBehaviour = trim((string) ($quizData['preferredbehaviour'] ?? ''));
if ($preferredBehaviour === '') {
$preferredBehaviour = 'deferredfeedback';
}
$gradeMethod = (int) ($quizData['grademethod'] ?? 1);
$decimalPoints = (int) ($quizData['decimalpoints'] ?? 2);
$questionsPerPage = (int) ($quizData['questionsperpage'] ?? 1);
$navMethod = (string) ($quizData['navmethod'] ?? 'free');
$shuffleAnswers = (int) ($quizData['shuffleanswers'] ?? 1);
$sumGrades = (float) ($quizData['sumgrades'] ?? 0.0);
$grade = (float) ($quizData['grade'] ?? 0.0);
$xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
$xmlContent .= '<activity id="'.$quizData['id'].'" moduleid="'.$quizData['moduleid'].'" modulename="quiz" contextid="'.$quizData['contextid'].'">'.PHP_EOL;
$xmlContent .= ' <quiz id="'.$quizData['id'].'">'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars($quizData['name']).'</name>'.PHP_EOL;
$xmlContent .= ' <intro>'.htmlspecialchars($quizData['intro']).'</intro>'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars((string) $quizData['name']).'</name>'.PHP_EOL;
$xmlContent .= ' <intro>'.htmlspecialchars((string) $quizData['intro']).'</intro>'.PHP_EOL;
$xmlContent .= ' <introformat>1</introformat>'.PHP_EOL;
$xmlContent .= ' <timeopen>'.($quizData['timeopen'] ?? 0).'</timeopen>'.PHP_EOL;
$xmlContent .= ' <timeclose>'.($quizData['timeclose'] ?? 0).'</timeclose>'.PHP_EOL;
$xmlContent .= ' <timelimit>'.($quizData['timelimit'] ?? 0).'</timelimit>'.PHP_EOL;
$xmlContent .= ' <overduehandling>'.($quizData['overduehandling'] ?? 'autosubmit').'</overduehandling>'.PHP_EOL;
$xmlContent .= ' <graceperiod>'.($quizData['graceperiod'] ?? 0).'</graceperiod>'.PHP_EOL;
$xmlContent .= ' <preferredbehaviour>'.htmlspecialchars($quizData['preferredbehaviour']).'</preferredbehaviour>'.PHP_EOL;
$xmlContent .= ' <preferredbehaviour>'.htmlspecialchars($preferredBehaviour).'</preferredbehaviour>'.PHP_EOL;
$xmlContent .= ' <canredoquestions>'.($quizData['canredoquestions'] ?? 0).'</canredoquestions>'.PHP_EOL;
$xmlContent .= ' <attempts_number>'.($quizData['attempts_number'] ?? 0).'</attempts_number>'.PHP_EOL;
$xmlContent .= ' <attemptonlast>'.($quizData['attemptonlast'] ?? 0).'</attemptonlast>'.PHP_EOL;
$xmlContent .= ' <grademethod>'.$quizData['grademethod'].'</grademethod>'.PHP_EOL;
$xmlContent .= ' <decimalpoints>'.$quizData['decimalpoints'].'</decimalpoints>'.PHP_EOL;
$xmlContent .= ' <grademethod>'.$gradeMethod.'</grademethod>'.PHP_EOL;
$xmlContent .= ' <decimalpoints>'.$decimalPoints.'</decimalpoints>'.PHP_EOL;
$xmlContent .= ' <questiondecimalpoints>'.($quizData['questiondecimalpoints'] ?? -1).'</questiondecimalpoints>'.PHP_EOL;
// Review options
$xmlContent .= ' <reviewattempt>'.($quizData['reviewattempt'] ?? 69888).'</reviewattempt>'.PHP_EOL;
$xmlContent .= ' <reviewcorrectness>'.($quizData['reviewcorrectness'] ?? 4352).'</reviewcorrectness>'.PHP_EOL;
$xmlContent .= ' <reviewmarks>'.($quizData['reviewmarks'] ?? 4352).'</reviewmarks>'.PHP_EOL;
@@ -288,36 +482,26 @@ class QuizExport extends ActivityExport
$xmlContent .= ' <reviewgeneralfeedback>'.($quizData['reviewgeneralfeedback'] ?? 4352).'</reviewgeneralfeedback>'.PHP_EOL;
$xmlContent .= ' <reviewrightanswer>'.($quizData['reviewrightanswer'] ?? 4352).'</reviewrightanswer>'.PHP_EOL;
$xmlContent .= ' <reviewoverallfeedback>'.($quizData['reviewoverallfeedback'] ?? 4352).'</reviewoverallfeedback>'.PHP_EOL;
// Navigation and presentation settings
$xmlContent .= ' <questionsperpage>'.$quizData['questionsperpage'].'</questionsperpage>'.PHP_EOL;
$xmlContent .= ' <navmethod>'.htmlspecialchars($quizData['navmethod']).'</navmethod>'.PHP_EOL;
$xmlContent .= ' <shuffleanswers>'.$quizData['shuffleanswers'].'</shuffleanswers>'.PHP_EOL;
$xmlContent .= ' <sumgrades>'.$quizData['sumgrades'].'</sumgrades>'.PHP_EOL;
$xmlContent .= ' <grade>'.$quizData['grade'].'</grade>'.PHP_EOL;
// Timing and security
$xmlContent .= ' <questionsperpage>'.$questionsPerPage.'</questionsperpage>'.PHP_EOL;
$xmlContent .= ' <navmethod>'.htmlspecialchars($navMethod).'</navmethod>'.PHP_EOL;
$xmlContent .= ' <shuffleanswers>'.$shuffleAnswers.'</shuffleanswers>'.PHP_EOL;
$xmlContent .= ' <sumgrades>'.$sumGrades.'</sumgrades>'.PHP_EOL;
$xmlContent .= ' <grade>'.$grade.'</grade>'.PHP_EOL;
$xmlContent .= ' <timecreated>'.($quizData['timecreated'] ?? time()).'</timecreated>'.PHP_EOL;
$xmlContent .= ' <timemodified>'.($quizData['timemodified'] ?? time()).'</timemodified>'.PHP_EOL;
$xmlContent .= ' <password>'.(isset($quizData['password']) ? htmlspecialchars($quizData['password']) : '').'</password>'.PHP_EOL;
$xmlContent .= ' <subnet>'.(isset($quizData['subnet']) ? htmlspecialchars($quizData['subnet']) : '').'</subnet>'.PHP_EOL;
$xmlContent .= ' <browsersecurity>'.(isset($quizData['browsersecurity']) ? htmlspecialchars($quizData['browsersecurity']) : '-').'</browsersecurity>'.PHP_EOL;
$xmlContent .= ' <password>'.(isset($quizData['password']) ? htmlspecialchars((string) $quizData['password']) : '').'</password>'.PHP_EOL;
$xmlContent .= ' <subnet>'.(isset($quizData['subnet']) ? htmlspecialchars((string) $quizData['subnet']) : '').'</subnet>'.PHP_EOL;
$xmlContent .= ' <browsersecurity>'.(isset($quizData['browsersecurity']) ? htmlspecialchars((string) $quizData['browsersecurity']) : '-').'</browsersecurity>'.PHP_EOL;
$xmlContent .= ' <delay1>'.($quizData['delay1'] ?? 0).'</delay1>'.PHP_EOL;
$xmlContent .= ' <delay2>'.($quizData['delay2'] ?? 0).'</delay2>'.PHP_EOL;
// Additional options
$xmlContent .= ' <showuserpicture>'.($quizData['showuserpicture'] ?? 0).'</showuserpicture>'.PHP_EOL;
$xmlContent .= ' <showblocks>'.($quizData['showblocks'] ?? 0).'</showblocks>'.PHP_EOL;
$xmlContent .= ' <completionattemptsexhausted>'.($quizData['completionattemptsexhausted'] ?? 0).'</completionattemptsexhausted>'.PHP_EOL;
$xmlContent .= ' <completionpass>'.($quizData['completionpass'] ?? 0).'</completionpass>'.PHP_EOL;
$xmlContent .= ' <completionminattempts>'.($quizData['completionminattempts'] ?? 0).'</completionminattempts>'.PHP_EOL;
$xmlContent .= ' <allowofflineattempts>'.($quizData['allowofflineattempts'] ?? 0).'</allowofflineattempts>'.PHP_EOL;
// Subplugin, if applicable
$xmlContent .= ' <subplugin_quizaccess_seb_quiz>'.PHP_EOL;
$xmlContent .= ' </subplugin_quizaccess_seb_quiz>'.PHP_EOL;
// Add question instances
$xmlContent .= ' <question_instances>'.PHP_EOL;
$slotIndex = 1;
foreach ($quizData['questions'] as $question) {
@@ -333,37 +517,28 @@ class QuizExport extends ActivityExport
$slotIndex++;
}
$xmlContent .= ' </question_instances>'.PHP_EOL;
// Quiz sections
$xmlContent .= ' <sections>'.PHP_EOL;
$xmlContent .= ' <section id="'.$quizData['id'].'">'.PHP_EOL;
$xmlContent .= ' <firstslot>1</firstslot>'.PHP_EOL;
$xmlContent .= ' <shufflequestions>0</shufflequestions>'.PHP_EOL;
$xmlContent .= ' </section>'.PHP_EOL;
$xmlContent .= ' </sections>'.PHP_EOL;
// Add feedbacks
$xmlContent .= ' <feedbacks>'.PHP_EOL;
foreach ($quizData['feedbacks'] as $feedback) {
$xmlContent .= ' <feedback id="'.$quizData['id'].'">'.PHP_EOL;
$xmlContent .= ' <feedbacktext>'.htmlspecialchars($feedback['feedbacktext']).'</feedbacktext>'.PHP_EOL;
$xmlContent .= ' <feedbacktext>'.htmlspecialchars((string) $feedback['feedbacktext']).'</feedbacktext>'.PHP_EOL;
$xmlContent .= ' <feedbacktextformat>1</feedbacktextformat>'.PHP_EOL;
$xmlContent .= ' <mingrade>'.$feedback['mingrade'].'</mingrade>'.PHP_EOL;
$xmlContent .= ' <maxgrade>'.$feedback['maxgrade'].'</maxgrade>'.PHP_EOL;
$xmlContent .= ' </feedback>'.PHP_EOL;
}
$xmlContent .= ' </feedbacks>'.PHP_EOL;
// Complete with placeholders for attempts and grades
$xmlContent .= ' <overrides>'.PHP_EOL.' </overrides>'.PHP_EOL;
$xmlContent .= ' <grades>'.PHP_EOL.' </grades>'.PHP_EOL;
$xmlContent .= ' <attempts>'.PHP_EOL.' </attempts>'.PHP_EOL;
// Close the activity tag
$xmlContent .= ' </quiz>'.PHP_EOL;
$xmlContent .= '</activity>'.PHP_EOL;
// Save the XML file
$xmlFile = $destinationDir.'/quiz.xml';
if (file_put_contents($xmlFile, $xmlContent) === false) {
throw new Exception(get_lang('ErrorCreatingQuizXml'));
@@ -404,10 +579,7 @@ class QuizExport extends ActivityExport
*/
private function exportMultichoiceNosingleQuestion(array $question): string
{
// Similar structure to exportMultichoiceQuestion, but with single=0
$xmlContent = str_replace('<single>1</single>', '<single>0</single>', $this->exportMultichoiceQuestion($question));
return $xmlContent;
return str_replace('<single>1</single>', '<single>0</single>', $this->exportMultichoiceQuestion($question));
}
/**
@@ -457,13 +629,13 @@ class QuizExport extends ActivityExport
private function exportMatchQuestion(array $question): string
{
$xmlContent = ' <plugin_qtype_match_question>'.PHP_EOL;
$xmlContent .= ' <matchoptions id="'.htmlspecialchars($question['id'] ?? '0').'">'.PHP_EOL;
$xmlContent .= ' <matchoptions id="'.htmlspecialchars((string) ($question['id'] ?? '0')).'">'.PHP_EOL;
$xmlContent .= ' <shuffleanswers>1</shuffleanswers>'.PHP_EOL;
$xmlContent .= ' <correctfeedback>'.htmlspecialchars($question['correctfeedback'] ?? '').'</correctfeedback>'.PHP_EOL;
$xmlContent .= ' <correctfeedback>'.htmlspecialchars((string) ($question['correctfeedback'] ?? '')).'</correctfeedback>'.PHP_EOL;
$xmlContent .= ' <correctfeedbackformat>0</correctfeedbackformat>'.PHP_EOL;
$xmlContent .= ' <partiallycorrectfeedback>'.htmlspecialchars($question['partiallycorrectfeedback'] ?? '').'</partiallycorrectfeedback>'.PHP_EOL;
$xmlContent .= ' <partiallycorrectfeedback>'.htmlspecialchars((string) ($question['partiallycorrectfeedback'] ?? '')).'</partiallycorrectfeedback>'.PHP_EOL;
$xmlContent .= ' <partiallycorrectfeedbackformat>0</partiallycorrectfeedbackformat>'.PHP_EOL;
$xmlContent .= ' <incorrectfeedback>'.htmlspecialchars($question['incorrectfeedback'] ?? '').'</incorrectfeedback>'.PHP_EOL;
$xmlContent .= ' <incorrectfeedback>'.htmlspecialchars((string) ($question['incorrectfeedback'] ?? '')).'</incorrectfeedback>'.PHP_EOL;
$xmlContent .= ' <incorrectfeedbackformat>0</incorrectfeedbackformat>'.PHP_EOL;
$xmlContent .= ' <shownumcorrect>0</shownumcorrect>'.PHP_EOL;
$xmlContent .= ' </matchoptions>'.PHP_EOL;
@@ -496,11 +668,185 @@ class QuizExport extends ActivityExport
private function exportAnswer(array $answer): string
{
return ' <answer id="'.($answer['id'] ?? '0').'">'.PHP_EOL.
' <answertext>'.htmlspecialchars($answer['text'] ?? 'No answer text').'</answertext>'.PHP_EOL.
' <answertext>'.htmlspecialchars((string) ($answer['text'] ?? 'No answer text')).'</answertext>'.PHP_EOL.
' <answerformat>1</answerformat>'.PHP_EOL.
' <fraction>'.($answer['fraction'] ?? '0').'</fraction>'.PHP_EOL.
' <feedback>'.htmlspecialchars($answer['feedback'] ?? '').'</feedback>'.PHP_EOL.
' <feedback>'.htmlspecialchars((string) ($answer['feedback'] ?? '')).'</feedback>'.PHP_EOL.
' <feedbackformat>1</feedbackformat>'.PHP_EOL.
' </answer>'.PHP_EOL;
}
private function ensureTrailingSlash(string $path): string
{
if ($path === '' || $path === '.' || $path === '/') {
return '/';
}
$path = preg_replace('#/+#', '/', $path);
return rtrim($path, '/').'/';
}
private function buildQuestionEmbeddedFileId(int $baseId, int $sequence): int
{
self::$embeddedFileGlobalSeq++;
return 1300000000 + self::$embeddedFileGlobalSeq;
}
/**
* Extract embedded /document/* images from HTML and rewrite src to @@PLUGINFILE@@.
*
* @return array{content:string, files:array<int,array<string,mixed>>}
*/
private function extractQuestionEmbeddedFilesAndNormalizeContent(
string $html,
int $contextId,
string $fileArea,
int $itemId,
string $prefix,
array $courseInfo,
int $adminId
): array {
if ($html === '') {
return ['content' => '', 'files' => []];
}
$fileExport = new FileExport($this->course);
$files = [];
$seenDocIds = [];
$sequence = 0;
$normalizedHtml = preg_replace_callback(
'#<img[^>]+src=["\'](?<url>[^"\']+)["\']#i',
function ($match) use (
$contextId,
$fileArea,
$itemId,
$courseInfo,
$adminId,
$fileExport,
&$files,
&$seenDocIds,
&$sequence
) {
$src = (string) ($match['url'] ?? '');
if (!preg_match('#/document(?P<path>/[^"\']+)#', $src, $m)) {
return $match[0];
}
$docRelPath = (string) $m['path'];
$docId = \DocumentManager::get_document_id($courseInfo, $docRelPath);
if (empty($docId)) {
return $match[0];
}
$docId = (int) $docId;
if (!isset($seenDocIds[$docId])) {
$doc = \DocumentManager::get_document_data_by_id($docId, $courseInfo['code']);
if (!empty($doc) && !empty($doc['path'])) {
$docPath = (string) $doc['path'];
$rel = ltrim(str_replace('\\', '/', $docPath), '/');
$dir = dirname($rel);
$dir = ($dir === '.' ? '' : $dir.'/');
$filepath = '/'.$dir;
$filepath = $this->ensureTrailingSlash($filepath);
$filename = basename($rel);
$absolutePath = $this->course->path.'document'.$docPath;
$sequence++;
$fileId = $this->buildQuestionEmbeddedFileId($itemId, $sequence);
$files[] = [
'id' => $fileId,
'contenthash' => is_file($absolutePath) ? sha1_file($absolutePath) : hash('sha1', $filename),
'contextid' => $contextId,
'component' => 'question',
'filearea' => $fileArea,
'itemid' => $itemId,
'filepath' => $filepath,
'documentpath' => 'document'.$docPath,
'filename' => $filename,
'userid' => $adminId,
'filesize' => (int) ($doc['size'] ?? 0),
'mimetype' => $fileExport->getMimeType($docPath),
'status' => 0,
'timecreated' => time() - 3600,
'timemodified' => time(),
'source' => (string) ($doc['title'] ?? $filename),
'author' => 'Unknown',
'license' => 'allrightsreserved',
];
}
$seenDocIds[$docId] = true;
}
$relHtml = ltrim($docRelPath, '/');
$pluginPath = '@@PLUGINFILE@@/'.$relHtml;
return str_replace($src, $pluginPath, $match[0]);
},
$html
);
return [
'content' => (string) $normalizedHtml,
'files' => $files,
];
}
/**
* Build a deterministic answer id for one question answer.
*/
private function buildStableQuestionAnswerId(int $questionId, int $answerIndex): int
{
return ($questionId * 1000) + $answerIndex;
}
/**
* Build a stable question-bank course context id for backup purposes.
*/
private function buildQuestionBankContextId(int $courseId): int
{
$contextId = MoodleExport::getBackupCourseContextId();
if ($contextId > 0) {
return $contextId;
}
return 700000000 + max(1, $courseId);
}
/**
* Build a clean question name from the question text.
*/
private function sanitizeQuestionName(string $raw, int $maxLen = 255): string
{
$text = trim($raw);
if ($text === '') {
return 'Question';
}
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$text = strip_tags($text);
$text = preg_replace('/\s+/u', ' ', $text);
$text = trim($text);
if ($text === '') {
return 'Question';
}
if (function_exists('mb_strlen') && function_exists('mb_substr')) {
if (mb_strlen($text, 'UTF-8') > $maxLen) {
$text = mb_substr($text, 0, $maxLen, 'UTF-8');
}
} elseif (strlen($text) > $maxLen) {
$text = substr($text, 0, $maxLen);
}
return $text;
}
}
+148 -19
View File
@@ -21,13 +21,13 @@ class ResourceExport extends ActivityExport
*/
public function export($activityId, $exportDir, $moduleId, $sectionId): void
{
// Prepare the directory where the resource export will be saved
$resourceDir = $this->prepareActivityDirectory($exportDir, 'resource', $moduleId);
$resourceDir = $this->prepareActivityDirectory($exportDir, 'resource', (int) $moduleId);
$resourceData = $this->getData((int) $activityId, (int) $sectionId, (int) $moduleId);
// Retrieve resource data
$resourceData = $this->getData($activityId, $sectionId);
if (empty($resourceData)) {
return;
}
// Generate XML files
$this->createResourceXml($resourceData, $resourceDir);
$this->createModuleXml($resourceData, $resourceDir);
$this->createGradesXml($resourceData, $resourceDir);
@@ -42,22 +42,48 @@ class ResourceExport extends ActivityExport
/**
* Get resource data dynamically from the course.
*/
public function getData(int $resourceId, int $sectionId): array
public function getData(int $resourceId, int $sectionId, ?int $moduleId = null): array
{
if (empty($this->course->resources[RESOURCE_DOCUMENT][$resourceId])) {
return [];
}
$resource = $this->course->resources[RESOURCE_DOCUMENT][$resourceId];
$name = (string) ($resource->title ?? '');
if ($sectionId > 0) {
$name = $this->lpItemTitle($sectionId, RESOURCE_DOCUMENT, $resourceId, $name);
}
$name = $this->sanitizeMoodleActivityName($name, 255);
$effectiveModuleId = (int) ($moduleId ?? $resource->source_id);
if ($effectiveModuleId <= 0) {
$effectiveModuleId = (int) $resource->source_id;
}
$resourceFile = $this->buildResourceFileEntry($resource, $effectiveModuleId);
$introResult = $this->extractEmbeddedFilesAndNormalizeContent(
(string) ($resource->comment ?? ''),
$effectiveModuleId,
'mod_resource',
'intro',
0,
fn (int $sequence): int => $this->buildResourceIntroFileId($effectiveModuleId, $sequence)
);
return [
'id' => $resourceId,
'moduleid' => $resource->source_id,
'moduleid' => $effectiveModuleId,
'modulename' => 'resource',
'contextid' => $resource->source_id,
'name' => $resource->title,
'intro' => $resource->comment ?? '',
'contextid' => $effectiveModuleId,
'name' => $name,
'intro' => $introResult['content'],
'sectionid' => $sectionId,
'sectionnumber' => 1,
'timemodified' => time(),
'users' => [],
'files' => [],
'files' => array_merge([$resourceFile], $introResult['files']),
];
}
@@ -72,18 +98,121 @@ class ResourceExport extends ActivityExport
$xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
$xmlContent .= '<inforef>'.PHP_EOL;
$xmlContent .= ' <fileref>'.PHP_EOL;
$xmlContent .= ' <file>'.PHP_EOL;
$xmlContent .= ' <id>'.htmlspecialchars($references['id']).'</id>'.PHP_EOL;
$xmlContent .= ' </file>'.PHP_EOL;
$xmlContent .= ' </fileref>'.PHP_EOL;
if (!empty($references['files']) && is_array($references['files'])) {
$xmlContent .= ' <fileref>'.PHP_EOL;
foreach ($references['files'] as $file) {
$fileId = is_array($file) ? (int) ($file['id'] ?? 0) : (int) $file;
if ($fileId <= 0) {
continue;
}
$xmlContent .= ' <file>'.PHP_EOL;
$xmlContent .= ' <id>'.$fileId.'</id>'.PHP_EOL;
$xmlContent .= ' </file>'.PHP_EOL;
}
$xmlContent .= ' </fileref>'.PHP_EOL;
}
$xmlContent .= '</inforef>'.PHP_EOL;
// Save the XML content to the directory
$this->createXmlFile('inforef', $xmlContent, $directory);
}
/**
* Build the files.xml entry for a resource activity file.
*/
private function buildResourceFileEntry(object $resource, int $moduleId): array
{
$adminData = MoodleExport::getAdminUserData();
$adminId = (int) ($adminData['id'] ?? 1);
$documentPath = (string) $resource->path;
$absolutePath = $this->course->path.$documentPath;
$filename = basename($documentPath);
$contenthash = is_file($absolutePath)
? sha1_file($absolutePath)
: hash('sha1', $filename);
return [
'id' => $this->buildResourceFileId($moduleId, (int) $resource->source_id),
'contenthash' => $contenthash,
'contextid' => $moduleId,
'component' => 'mod_resource',
'filearea' => 'content',
'itemid' => 0,
'filepath' => '/',
'documentpath' => $documentPath,
'filename' => $filename,
'userid' => $adminId,
'filesize' => (int) ($resource->size ?? 0),
'mimetype' => $this->guessMimeType($documentPath),
'status' => 0,
'timecreated' => time() - 3600,
'timemodified' => time(),
'source' => (string) ($resource->title ?? $filename),
'author' => 'Unknown',
'license' => 'allrightsreserved',
];
}
/**
* Build a stable file id for mod_resource main file entries.
*/
private function buildResourceFileId(int $moduleId, int $resourceId): int
{
$base = $moduleId > 0 ? $moduleId : $resourceId;
return 1000000000 + $base;
}
/**
* Build a stable file id for embedded intro files in mod_resource.
*/
private function buildResourceIntroFileId(int $moduleId, int $sequence): int
{
return 1250000000 + max(0, $moduleId) + max(1, $sequence);
}
/**
* Guess MIME type from file extension.
*/
private function guessMimeType(string $filePath): string
{
$ext = strtolower((string) pathinfo($filePath, PATHINFO_EXTENSION));
$map = [
'pdf' => 'application/pdf',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'svg' => 'image/svg+xml',
'webp' => 'image/webp',
'html' => 'text/html',
'htm' => 'text/html',
'txt' => 'text/plain',
'css' => 'text/css',
'js' => 'application/javascript',
'mp4' => 'video/mp4',
'webm' => 'video/webm',
'mp3' => 'audio/mpeg',
'ogg' => 'audio/ogg',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls' => 'application/vnd.ms-excel',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'ppt' => 'application/vnd.ms-powerpoint',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'zip' => 'application/zip',
'rar' => 'application/x-rar-compressed',
];
return $map[$ext] ?? 'application/octet-stream';
}
/**
* Create the XML file for the resource.
*/
@@ -92,8 +221,8 @@ class ResourceExport extends ActivityExport
$xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
$xmlContent .= '<activity id="'.$resourceData['id'].'" moduleid="'.$resourceData['moduleid'].'" modulename="resource" contextid="'.$resourceData['contextid'].'">'.PHP_EOL;
$xmlContent .= ' <resource id="'.$resourceData['id'].'">'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars($resourceData['name']).'</name>'.PHP_EOL;
$xmlContent .= ' <intro>'.htmlspecialchars($resourceData['intro']).'</intro>'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars((string) $resourceData['name']).'</name>'.PHP_EOL;
$xmlContent .= ' <intro><![CDATA['.(string) $resourceData['intro'].']]></intro>'.PHP_EOL;
$xmlContent .= ' <introformat>1</introformat>'.PHP_EOL;
$xmlContent .= ' <tobemigrated>0</tobemigrated>'.PHP_EOL;
$xmlContent .= ' <legacyfiles>0</legacyfiles>'.PHP_EOL;
+161 -55
View File
@@ -15,15 +15,17 @@ use Exception;
class SectionExport
{
private $course;
private $activitiesBySection = [];
/**
* Constructor to initialize the course object.
*
* @param object $course The course object to be exported.
*/
public function __construct($course)
public function __construct($course, array $activitiesBySection = [])
{
$this->course = $course;
$this->activitiesBySection = $activitiesBySection;
}
/**
@@ -47,8 +49,8 @@ class SectionExport
$sectionData = [
'id' => 0,
'number' => 0,
'name' => get_lang('General'),
'summary' => get_lang('GeneralResourcesCourse'),
'name' => $this->sanitizeText((string) get_lang('General')),
'summary' => (string) get_lang('GeneralResourcesCourse'),
'sequence' => 0,
'visible' => 1,
'timemodified' => time(),
@@ -68,7 +70,6 @@ class SectionExport
{
$generalItems = [];
// List of resource types and their corresponding ID keys
$resourceTypes = [
RESOURCE_DOCUMENT => 'source_id',
RESOURCE_QUIZ => 'source_id',
@@ -85,8 +86,9 @@ class SectionExport
foreach ($this->course->resources[$resourceType] as $id => $resource) {
if (!$this->isItemInLearnpath($resource, $resourceType)) {
$title = $resourceType === RESOURCE_WORK
? ($resource->params['title'] ?? '')
: ($resource->title ?? $resource->name);
? (string) ($resource->params['title'] ?? '')
: (string) ($resource->title ?? $resource->name ?? '');
$generalItems[] = [
'id' => $resource->$idKey,
'item_type' => $resourceType,
@@ -106,6 +108,10 @@ class SectionExport
*/
public function getActivitiesForGeneral(): array
{
if (isset($this->activitiesBySection[0]) && is_array($this->activitiesBySection[0])) {
return $this->activitiesBySection[0];
}
$generalLearnpath = (object) [
'items' => $this->getGeneralItems(),
'source_id' => 0,
@@ -113,7 +119,7 @@ class SectionExport
$activities = $this->getActivitiesForSection($generalLearnpath, true);
if (!in_array('folder', array_column($activities, 'modulename'))) {
if (!in_array('folder', array_column($activities, 'modulename'), true)) {
$activities[] = [
'id' => 0,
'moduleid' => 0,
@@ -132,7 +138,7 @@ class SectionExport
public function getLearnpathById(int $sectionId): ?object
{
foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) {
if ($learnpath->source_id == $sectionId) {
if ((int) $learnpath->source_id === $sectionId) {
return $learnpath;
}
}
@@ -145,14 +151,16 @@ class SectionExport
*/
public function getSectionData(object $learnpath): array
{
$sectionId = (int) $learnpath->source_id;
return [
'id' => $learnpath->source_id,
'number' => $learnpath->display_order,
'name' => $learnpath->name,
'summary' => $learnpath->description,
'sequence' => $learnpath->source_id,
'visible' => $learnpath->visibility,
'timemodified' => strtotime($learnpath->modified_on),
'id' => $sectionId,
'number' => (int) ($learnpath->display_order ?? 0),
'name' => $this->sanitizeText((string) ($learnpath->name ?? '')),
'summary' => (string) ($learnpath->description ?? ''),
'sequence' => $sectionId,
'visible' => 1,
'timemodified' => !empty($learnpath->modified_on) ? (int) strtotime((string) $learnpath->modified_on) : time(),
'activities' => $this->getActivitiesForSection($learnpath),
];
}
@@ -162,9 +170,13 @@ class SectionExport
*/
public function getActivitiesForSection(object $learnpath, bool $isGeneral = false): array
{
$activities = [];
$sectionId = $isGeneral ? 0 : $learnpath->source_id;
$sectionId = $isGeneral ? 0 : (int) $learnpath->source_id;
if (isset($this->activitiesBySection[$sectionId]) && is_array($this->activitiesBySection[$sectionId])) {
return $this->activitiesBySection[$sectionId];
}
$activities = [];
foreach ($learnpath->items as $item) {
$this->addActivityToList($item, $sectionId, $activities);
}
@@ -195,22 +207,76 @@ class SectionExport
$exportClass = new $exportClasses[$moduleName]($this->course);
$exportClass->export($activity['id'], $exportDir, $activity['moduleid'], $sectionId);
} else {
throw new \Exception("Export for module '$moduleName' is not supported.");
throw new Exception("Export for module '$moduleName' is not supported.");
}
}
}
/**
* Normalize resource / LP item types for reliable comparison.
*/
private function normalizeItemTypeForLpComparison(string $type): string
{
switch ($type) {
case 'student_publication':
case 'work':
return 'work';
case 'link':
case 'url':
return 'link';
case 'survey':
case 'feedback':
return 'survey';
default:
return $type;
}
}
/**
* Check if an item is associated with any learnpath.
*/
private function isItemInLearnpath(object $item, string $type): bool
{
if (!empty($this->course->resources[RESOURCE_LEARNPATH])) {
foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) {
if (!empty($learnpath->items)) {
foreach ($learnpath->items as $learnpathItem) {
if ($learnpathItem['item_type'] === $type && $learnpathItem['path'] == $item->source_id) {
return true;
if (empty($this->course->resources[RESOURCE_LEARNPATH])) {
return false;
}
$normalizedType = $this->normalizeItemTypeForLpComparison($type);
$itemSourceId = isset($item->source_id) ? (string) $item->source_id : '';
foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) {
if (empty($learnpath->items)) {
continue;
}
foreach ($learnpath->items as $learnpathItem) {
$lpType = isset($learnpathItem['item_type'])
? $this->normalizeItemTypeForLpComparison((string) $learnpathItem['item_type'])
: '';
if ($lpType !== $normalizedType) {
continue;
}
$lpPath = isset($learnpathItem['path']) ? (string) $learnpathItem['path'] : '';
if ($itemSourceId !== '' && $lpPath === $itemSourceId) {
return true;
}
if ($normalizedType === 'document' && $itemSourceId !== '' && ctype_digit($itemSourceId)) {
$doc = \DocumentManager::get_document_data_by_id((int) $itemSourceId, $this->course->code);
if (!empty($doc['path'])) {
$docPath = (string) $doc['path'];
$candidates = [$docPath, 'document/'.$docPath, '/'.$docPath];
foreach ($candidates as $candidate) {
if ($lpPath === $candidate) {
return true;
}
}
}
}
@@ -249,14 +315,14 @@ class SectionExport
'feedback' => FeedbackExport::class,
];
if ($item['id'] == 'course_homepage') {
if (($item['id'] ?? null) === 'course_homepage') {
$item['item_type'] = 'page';
$item['path'] = 0;
}
$itemType = $item['item_type'] === 'link' ? 'url' :
($item['item_type'] === 'work' || $item['item_type'] === 'student_publication' ? 'assign' :
($item['item_type'] === 'survey' ? 'feedback' : $item['item_type']));
$itemType = ($item['item_type'] ?? '') === 'link' ? 'url'
: ((($item['item_type'] ?? '') === 'work' || ($item['item_type'] ?? '') === 'student_publication') ? 'assign'
: ((($item['item_type'] ?? '') === 'survey') ? 'feedback' : ($item['item_type'] ?? '')));
switch ($itemType) {
case 'quiz':
@@ -266,41 +332,48 @@ class SectionExport
case 'forum':
case 'feedback':
case 'page':
$activityId = $itemType === 'glossary' ? 1 : (int) $item['path'];
$activityId = $itemType === 'glossary' ? 1 : (int) ($item['path'] ?? 0);
$exportClass = $activityClassMap[$itemType];
$exportInstance = new $exportClass($this->course);
$activityData = $exportInstance->getData($activityId, $sectionId);
break;
case 'document':
$documentId = (int) $item['path'];
$documentId = (int) ($item['path'] ?? 0);
$document = \DocumentManager::get_document_data_by_id($documentId, $this->course->code);
if ($document) {
$isRoot = substr_count($document['path'], '/') === 1;
$documentType = $this->getDocumentType($document['filetype'], $document['path']);
if ($documentType === 'page' && $isRoot) {
$activityClass = $activityClassMap['page'];
$exportInstance = new $activityClass($this->course);
$activityData = $exportInstance->getData($item['path'], $sectionId);
}
elseif ($sectionId > 0 && $documentType && isset($activityClassMap[$documentType])) {
$documentType = $this->getDocumentType((string) $document['filetype'], (string) $document['path']);
if ($sectionId > 0 && $documentType && isset($activityClassMap[$documentType])) {
$activityClass = $activityClassMap[$documentType];
$exportInstance = new $activityClass($this->course);
$activityData = $exportInstance->getData($item['path'], $sectionId);
$activityData = $exportInstance->getData($documentId, $sectionId);
} elseif ($sectionId === 0 && $documentType === 'page') {
$activityClass = $activityClassMap['page'];
$exportInstance = new $activityClass($this->course);
$activityData = $exportInstance->getData($documentId, $sectionId);
}
}
break;
}
// Add the activity to the list if the data exists
if (!empty($activityData) && $sectionId > 0) {
$lpItemId = isset($item['id']) ? (int) $item['id'] : 0;
$modName = (string) ($activityData['modulename'] ?? '');
if ($lpItemId > 0 && !in_array($modName, ['folder', 'glossary'], true)) {
$activityData['moduleid'] = 900000000 + $lpItemId;
}
}
if ($activityData) {
$activities[] = [
'id' => $activityData['id'],
'moduleid' => $activityData['moduleid'],
'type' => $item['item_type'],
'modulename' => $activityData['modulename'],
'name' => $activityData['name'],
'name' => $this->sanitizeText((string) ($activityData['name'] ?? '')),
];
}
}
@@ -310,13 +383,15 @@ class SectionExport
*/
private function getDocumentType(string $filetype, string $path): ?string
{
if ('html' === pathinfo($path, PATHINFO_EXTENSION)) {
$ext = strtolower((string) pathinfo($path, PATHINFO_EXTENSION));
if ($ext === 'html' || $ext === 'htm') {
return 'page';
} elseif ('file' === $filetype) {
}
if ($filetype === 'file') {
return 'resource';
} /*elseif ('folder' === $filetype) {
return 'folder';
}*/
}
return null;
}
@@ -326,19 +401,20 @@ class SectionExport
*/
private function createSectionXml(array $sectionData, string $destinationDir): void
{
$sequence = implode(',', array_column($sectionData['activities'], 'moduleid'));
$xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
$xmlContent .= '<section id="'.$sectionData['id'].'">'.PHP_EOL;
$xmlContent .= ' <number>'.$sectionData['number'].'</number>'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars($sectionData['name']).'</name>'.PHP_EOL;
$xmlContent .= ' <summary>'.htmlspecialchars($sectionData['summary']).'</summary>'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars((string) $sectionData['name']).'</name>'.PHP_EOL;
$xmlContent .= ' <summary>'.htmlspecialchars((string) $sectionData['summary']).'</summary>'.PHP_EOL;
$xmlContent .= ' <summaryformat>1</summaryformat>'.PHP_EOL;
$xmlContent .= ' <sequence>'.implode(',', array_column($sectionData['activities'], 'moduleid')).'</sequence>'.PHP_EOL;
$xmlContent .= ' <visible>'.$sectionData['visible'].'</visible>'.PHP_EOL;
$xmlContent .= ' <sequence>'.$sequence.'</sequence>'.PHP_EOL;
$xmlContent .= ' <visible>1</visible>'.PHP_EOL;
$xmlContent .= ' <timemodified>'.$sectionData['timemodified'].'</timemodified>'.PHP_EOL;
$xmlContent .= '</section>'.PHP_EOL;
$xmlFile = $destinationDir.'/section.xml';
file_put_contents($xmlFile, $xmlContent);
file_put_contents($destinationDir.'/section.xml', $xmlContent);
}
/**
@@ -350,12 +426,42 @@ class SectionExport
$xmlContent .= '<inforef>'.PHP_EOL;
foreach ($sectionData['activities'] as $activity) {
$xmlContent .= ' <activity id="'.$activity['id'].'">'.htmlspecialchars($activity['name']).'</activity>'.PHP_EOL;
$refId = $activity['moduleid'] ?? $activity['id'];
$xmlContent .= ' <activity id="'.$refId.'">'.htmlspecialchars((string) ($activity['name'] ?? '')).'</activity>'.PHP_EOL;
}
$xmlContent .= '</inforef>'.PHP_EOL;
$xmlFile = $destinationDir.'/inforef.xml';
file_put_contents($xmlFile, $xmlContent);
file_put_contents($destinationDir.'/inforef.xml', $xmlContent);
}
/**
* Sanitize section or activity titles for Moodle.
*/
private function sanitizeText(string $raw, int $maxLen = 255): string
{
$text = trim($raw);
if ($text === '') {
return '';
}
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$text = strip_tags($text);
$text = preg_replace('/\s+/u', ' ', $text);
$text = trim($text);
if ($text === '') {
return '';
}
if (function_exists('mb_strlen') && function_exists('mb_substr')) {
if (mb_strlen($text, 'UTF-8') > $maxLen) {
$text = mb_substr($text, 0, $maxLen, 'UTF-8');
}
} elseif (strlen($text) > $maxLen) {
$text = substr($text, 0, $maxLen);
}
return $text;
}
}
+55 -20
View File
@@ -12,22 +12,27 @@ namespace moodleexport;
class UrlExport extends ActivityExport
{
/**
* Export all URL resources into a single Moodle activity.
* Export one URL activity.
*
* @param int $activityId The ID of the URL.
* @param string $exportDir The directory where the URL will be exported.
* @param int $moduleId The ID of the module.
* @param int $moduleId The exported module ID.
* @param int $sectionId The ID of the section.
*/
public function export($activityId, $exportDir, $moduleId, $sectionId): void
{
// Prepare the directory where the URL export will be saved
$urlDir = $this->prepareActivityDirectory($exportDir, 'url', $moduleId);
$effectiveModuleId = (int) $moduleId;
if ($effectiveModuleId <= 0) {
$effectiveModuleId = (int) $activityId;
}
// Retrieve URL data
$urlData = $this->getData($activityId, $sectionId);
$urlDir = $this->prepareActivityDirectory($exportDir, 'url', $effectiveModuleId);
$urlData = $this->getData((int) $activityId, (int) $sectionId, $effectiveModuleId);
if (empty($urlData)) {
return;
}
// Generate XML file for the URL
$this->createUrlXml($urlData, $urlDir);
$this->createModuleXml($urlData, $urlDir);
$this->createGradesXml($urlData, $urlDir);
@@ -40,28 +45,50 @@ class UrlExport extends ActivityExport
}
/**
* Get all URL data for the course.
* Get URL data for the course.
*/
public function getData(int $activityId, int $sectionId): ?array
public function getData(int $activityId, int $sectionId, ?int $moduleId = null): ?array
{
// Extract the URL information from the course data
if (empty($this->course->resources['link'][$activityId])) {
return null;
}
$url = $this->course->resources['link'][$activityId];
// Return the URL data formatted for export
$effectiveModuleId = (int) ($moduleId ?? $activityId);
if ($effectiveModuleId <= 0) {
$effectiveModuleId = $activityId;
}
$name = (string) ($url->title ?? '');
if ($sectionId > 0) {
$name = $this->lpItemTitle($sectionId, RESOURCE_LINK, $activityId, $name);
}
$name = $this->sanitizeMoodleActivityName($name, 255);
$descriptionResult = $this->extractEmbeddedFilesAndNormalizeContent(
(string) ($url->description ?? ''),
$effectiveModuleId,
'mod_url',
'intro',
0,
fn (int $sequence): int => $this->buildUrlEmbeddedFileId($effectiveModuleId, $sequence)
);
return [
'id' => $activityId,
'moduleid' => $activityId,
'moduleid' => $effectiveModuleId,
'modulename' => 'url',
'contextid' => $this->course->info['real_id'],
'name' => $url->title,
'description' => $url->description,
'externalurl' => $url->url,
'contextid' => $effectiveModuleId,
'name' => $name,
'description' => $descriptionResult['content'],
'externalurl' => (string) ($url->url ?? ''),
'timecreated' => time(),
'timemodified' => time(),
'sectionid' => $sectionId,
'sectionnumber' => 0,
'users' => [],
'files' => [],
'files' => $descriptionResult['files'],
];
}
@@ -73,10 +100,10 @@ class UrlExport extends ActivityExport
$xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
$xmlContent .= '<activity id="'.$urlData['id'].'" moduleid="'.$urlData['moduleid'].'" modulename="'.$urlData['modulename'].'" contextid="'.$urlData['contextid'].'">'.PHP_EOL;
$xmlContent .= ' <url id="'.$urlData['id'].'">'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars($urlData['name']).'</name>'.PHP_EOL;
$xmlContent .= ' <intro><![CDATA['.htmlspecialchars($urlData['description']).']]></intro>'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars((string) $urlData['name']).'</name>'.PHP_EOL;
$xmlContent .= ' <intro><![CDATA['.(string) $urlData['description'].']]></intro>'.PHP_EOL;
$xmlContent .= ' <introformat>1</introformat>'.PHP_EOL;
$xmlContent .= ' <externalurl>'.htmlspecialchars($urlData['externalurl']).'</externalurl>'.PHP_EOL;
$xmlContent .= ' <externalurl>'.htmlspecialchars((string) $urlData['externalurl']).'</externalurl>'.PHP_EOL;
$xmlContent .= ' <display>0</display>'.PHP_EOL;
$xmlContent .= ' <displayoptions>a:1:{s:10:"printintro";i:1;}</displayoptions>'.PHP_EOL;
$xmlContent .= ' <parameters>a:0:{}</parameters>'.PHP_EOL;
@@ -86,4 +113,12 @@ class UrlExport extends ActivityExport
$this->createXmlFile('url', $xmlContent, $urlDir);
}
/**
* Build a stable embedded file id for URL intro files.
*/
private function buildUrlEmbeddedFileId(int $moduleId, int $sequence): int
{
return 1200000000 + max(0, $moduleId) + max(1, $sequence);
}
}
+568 -54
View File
@@ -19,58 +19,20 @@ class MySpace
public static function getAdminActions()
{
$actions = [
[
'url' => api_get_path(WEB_CODE_PATH).'mySpace/admin_view.php?display=coaches',
'content' => get_lang('DisplayCoaches'),
],
[
'url' => api_get_path(WEB_CODE_PATH).'mySpace/admin_view.php?display=user',
'content' => get_lang('DisplayUserOverview'),
],
[
'url' => api_get_path(WEB_CODE_PATH).'mySpace/admin_view.php?display=session',
'content' => get_lang('DisplaySessionOverview'),
],
[
'url' => api_get_path(WEB_CODE_PATH).'mySpace/admin_view.php?display=course',
'content' => get_lang('DisplayCourseOverview'),
],
[
'url' => api_get_path(WEB_CODE_PATH).'tracking/question_course_report.php?view=admin',
'content' => get_lang('LPQuestionListResults'),
],
[
'url' => api_get_path(WEB_CODE_PATH).'tracking/course_session_report.php?view=admin',
'content' => get_lang('LPExerciseResultsBySession'),
],
[
'url' => api_get_path(WEB_CODE_PATH).'mySpace/admin_view.php?display=accessoverview',
'content' => get_lang('DisplayAccessOverview').' ('.get_lang('Beta').')',
],
[
'url' => api_get_path(WEB_CODE_PATH).'mySpace/exercise_category_report.php',
'content' => get_lang('ExerciseCategoryAllSessionsReport'),
],
[
'url' => api_get_path(WEB_CODE_PATH).'mySpace/survey_report.php',
'content' => get_lang('SurveysReport'),
],
[
'url' => api_get_path(WEB_CODE_PATH).'mySpace/tc_report.php',
'content' => get_lang('TCReport'),
],
[
'url' => api_get_path(WEB_CODE_PATH).'mySpace/ti_report.php',
'content' => get_lang('TIReport'),
],
[
'url' => api_get_path(WEB_CODE_PATH).'mySpace/question_stats_global.php',
'content' => get_lang('QuestionStats'),
],
[
'url' => api_get_path(WEB_CODE_PATH).'mySpace/question_stats_global_detail.php',
'content' => get_lang('ExerciseAttemptStatsReport'),
],
['url' => api_get_path(WEB_CODE_PATH).'mySpace/admin_view.php?display=coaches', 'content' => get_lang('DisplayCoaches')],
['url' => api_get_path(WEB_CODE_PATH).'mySpace/admin_view.php?display=user', 'content' => get_lang('DisplayUserOverview')],
['url' => api_get_path(WEB_CODE_PATH).'mySpace/admin_view.php?display=session', 'content' => get_lang('DisplaySessionOverview')],
['url' => api_get_path(WEB_CODE_PATH).'mySpace/admin_view.php?display=course', 'content' => get_lang('DisplayCourseOverview')],
['url' => api_get_path(WEB_CODE_PATH).'tracking/question_course_report.php?view=admin', 'content' => get_lang('LPQuestionListResults')],
['url' => api_get_path(WEB_CODE_PATH).'tracking/course_session_report.php?view=admin', 'content' => get_lang('LPExerciseResultsBySession')],
['url' => api_get_path(WEB_CODE_PATH).'mySpace/admin_view.php?display=accessoverview', 'content' => get_lang('DisplayAccessOverview').' ('.get_lang('Beta').')'],
['url' => api_get_path(WEB_CODE_PATH).'mySpace/exercise_category_report.php', 'content' => get_lang('ExerciseCategoryAllSessionsReport')],
['url' => api_get_path(WEB_CODE_PATH).'mySpace/survey_report.php', 'content' => get_lang('SurveysReport')],
['url' => api_get_path(WEB_CODE_PATH).'mySpace/tc_report.php', 'content' => get_lang('TCReport')],
['url' => api_get_path(WEB_CODE_PATH).'mySpace/ti_report.php', 'content' => get_lang('TIReport')],
['url' => api_get_path(WEB_CODE_PATH).'mySpace/question_stats_global.php', 'content' => get_lang('QuestionStats')],
['url' => api_get_path(WEB_CODE_PATH).'mySpace/question_stats_global_detail.php', 'content' => get_lang('ExerciseAttemptStatsReport')],
['url' => api_get_path(WEB_CODE_PATH).'mySpace/duplicated_users.php', 'content' => get_lang('DuplicatedUsers')],
];
$field = new ExtraField('user');
@@ -98,7 +60,22 @@ class MySpace
];
}
return Display::actions($actions, null);
$html = '<div class="clearfix" style="margin:8px 0 14px;">';
foreach ($actions as $a) {
$label = $a['content'];
if (stripos($label, '(Beta)') !== false) {
$label = str_ireplace('(Beta)', '<small class="text-muted">(Beta)</small>', $label);
}
$html .= '<a href="'.$a['url'].'" class="btn btn-default btn-sm" '.
'style="margin:4px 6px; border:1px solid #e1e5eb; border-radius:9999px; background:#fff;">'
.$label.
'</a>';
}
$html .= '</div>';
return $html;
}
/**
@@ -1515,7 +1492,7 @@ class MySpace
/**
* Displays a list as a table of teachers who are set authors of lp's item by a extra_field authors.
*/
public static function displayResumeLpByItem(string $startDate = null, string $endDate = null, bool $csv = false)
public static function displayResumeLpByItem(?string $startDate = null, ?string $endDate = null, bool $csv = false)
{
$tableHtml = '';
$table = '';
@@ -4397,6 +4374,464 @@ class MySpace
return get_lang('NoEntity');
}
/** Return all user extra fields as variable => human label. */
public static function duGetUserExtraFields(): array
{
$ef = new ExtraField('user');
$list = $ef->get_all();
$out = [];
if (!empty($list)) {
foreach ($list as $row) {
$out[$row['variable']] = $row['display_text'].' ('.$row['variable'].')';
}
ksort($out);
}
return $out;
}
/** Return user extra field info by variable name. */
public static function duGetUserExtraFieldByVariable(string $var)
{
$ef = new ExtraField('user');
return $ef->get_handler_field_info_by_field_variable($var);
}
/** Table exists? (current DB) */
public static function duTableExists(string $table): bool
{
$table = Database::escape_string($table);
$res = Database::query("SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = '$table' LIMIT 1");
return $res && Database::num_rows($res) > 0;
}
/** Column exists in table? */
public static function duColumnExists(string $table, string $column): bool
{
$table = Database::escape_string($table);
$column = Database::escape_string($column);
$res = Database::query("
SELECT 1
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = '$table'
AND column_name = '$column'
LIMIT 1
");
return $res && Database::num_rows($res) > 0;
}
/**
* Safe UPDATE (with pre-dedup when composite UNIQUE/PK exists).
*
* @param array $uniqueOn Columns (excluding user_id) that are part of a UNIQUE/PK and may collide when moving.
*/
public static function duSafeUpdateUserRef(string $table, string $column, int $fromUserId, int $toUserId, array $uniqueOn = []): int
{
if (!self::duTableExists($table) || !self::duColumnExists($table, $column)) {
return 0;
}
// Pre-deduplicate if composite key exists
if (!empty($uniqueOn)) {
$onParts = [];
foreach ($uniqueOn as $c) {
if (!self::duColumnExists($table, $c)) {
// if any listed unique column does not exist, skip dedup and proceed to UPDATE
$onParts = [];
break;
}
$onParts[] = "a.`$c` = b.`$c`";
}
if (!empty($onParts)) {
$onSql = implode(' AND ', $onParts);
$sqlDel = "
DELETE a
FROM `$table` a
JOIN `$table` b
ON $onSql
AND b.`$column` = $toUserId
WHERE a.`$column` = $fromUserId
";
$resDel = Database::query($sqlDel);
// If you need to log deletes: $delRows = Database::affected_rows($resDel);
}
}
// Move references (UPDATE IGNORE avoids duplicates)
$sqlUpd = "UPDATE IGNORE `$table` SET `$column` = $toUserId WHERE `$column` = $fromUserId";
$resUpd = Database::query($sqlUpd);
return Database::affected_rows($resUpd);
}
/**
* Safe UPDATE for tables with two user columns (e.g. user_rel_user).
* Allows independent dedup per column.
*
* @return array [updatedColA, updatedColB]
*/
public static function duSafeUpdateUserRefDual(
string $table,
string $colA,
string $colB,
int $fromUserId,
int $toUserId,
array $uniqueOnA = [],
array $uniqueOnB = []
): array {
$totA = 0;
$totB = 0;
if (!self::duTableExists($table)) {
return [$totA, $totB];
}
if (self::duColumnExists($table, $colA)) {
// Dedup for colA → toUserId
if (!empty($uniqueOnA)) {
$onParts = [];
foreach ($uniqueOnA as $c) {
if (!self::duColumnExists($table, $c)) {
$onParts = [];
break;
}
$onParts[] = "a.`$c` = b.`$c`";
}
if (!empty($onParts)) {
$onSql = implode(' AND ', $onParts);
$sqlDelA = "
DELETE a
FROM `$table` a
JOIN `$table` b
ON $onSql
AND b.`$colA` = $toUserId
WHERE a.`$colA` = $fromUserId
";
Database::query($sqlDelA);
}
}
$res = Database::query("UPDATE IGNORE `$table` SET `$colA` = $toUserId WHERE `$colA` = $fromUserId");
$totA = Database::affected_rows($res);
}
if (self::duColumnExists($table, $colB)) {
if (!empty($uniqueOnB)) {
$onParts = [];
foreach ($uniqueOnB as $c) {
if (!self::duColumnExists($table, $c)) {
$onParts = [];
break;
}
$onParts[] = "a.`$c` = b.`$c`";
}
if (!empty($onParts)) {
$onSql = implode(' AND ', $onParts);
$sqlDelB = "
DELETE a
FROM `$table` a
JOIN `$table` b
ON $onSql
AND b.`$colB` = $toUserId
WHERE a.`$colB` = $fromUserId
";
Database::query($sqlDelB);
}
}
$res1 = Database::query("UPDATE IGNORE `$table` SET `$colB` = $toUserId WHERE `$colB` = $fromUserId");
$totB = Database::affected_rows($res1);
}
return [$totA, $totB];
}
/** REPLACE(search, replace) in a text column (with LIKE filter). */
public static function duReplaceInColumn(string $table, string $column, string $search, string $replace): int
{
if (!self::duTableExists($table) || !self::duColumnExists($table, $column)) {
return 0;
}
$search = Database::escape_string($search);
$replace = Database::escape_string($replace);
$sql = "UPDATE `$table`
SET `$column` = REPLACE(`$column`, '$search', '$replace')
WHERE `$column` LIKE '%$search%'";
$res = Database::query($sql);
return Database::affected_rows($res);
}
/** Duplicated values for a given extra-field on the current portal. */
public static function duGetDuplicateValues(int $fieldId, int $urlId): array
{
$tblVals = Database::get_main_table(TABLE_EXTRA_FIELD_VALUES);
$tblUser = Database::get_main_table(TABLE_MAIN_USER);
$tblRel = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
$sql = "SELECT v.value AS the_value, COUNT(DISTINCT u.id) AS qty
FROM $tblVals v
JOIN $tblUser u ON u.id = v.item_id
JOIN $tblRel r ON r.user_id = u.id AND r.access_url_id = $urlId
WHERE v.field_id = $fieldId AND v.value <> ''
GROUP BY v.value
HAVING COUNT(DISTINCT u.id) > 1
ORDER BY qty DESC, the_value ASC";
$res = Database::query($sql);
return Database::store_result($res, 'ASSOC');
}
/** Users having exactly a value for the given extra-field (filtered by portal). */
public static function duGetUsersByFieldValue(int $fieldId, int $urlId, string $value, string $userStatusList = ''): array
{
$tblVals = Database::get_main_table(TABLE_EXTRA_FIELD_VALUES);
$tblUser = Database::get_main_table(TABLE_MAIN_USER);
$tblRel = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
$valueSql = "'".Database::escape_string($value)."'";
$filterUserStatus = "";
if ($userStatusList != '') {
$filterUserStatus = "AND u.status in (".$userStatusList.")";
}
$sql = "SELECT DISTINCT u.id AS user_id, u.username, u.firstname, u.lastname, u.email, u.registration_date
FROM $tblUser u
JOIN $tblRel r ON r.user_id = u.id AND r.access_url_id = $urlId
JOIN $tblVals v ON v.item_id = u.id AND v.field_id = $fieldId
WHERE v.value = $valueSql
$filterUserStatus
ORDER BY u.id ASC";
$res = Database::query($sql);
return Database::store_result($res, 'ASSOC');
}
/** Deactivate (active=0) or delete user with API if available. */
public static function duDisableOrDeleteUser(int $userId, string $mode): bool
{
$mode = $mode === 'delete' ? 'delete' : 'deactivate';
if ($mode === 'delete' && class_exists('UserManager') && method_exists('UserManager', 'delete_user')) {
return UserManager::delete_user($userId);
}
// Deactivate
$tblUser = Database::get_main_table(TABLE_MAIN_USER);
$sql = "UPDATE $tblUser SET active = 0 WHERE user_id = $userId";
return (bool) Database::query($sql);
}
/**
* Move ALL references from $fromUserId $toUserId, including special cases.
* Returns a log like "table.col (n)" for UI display.
*/
public static function duUpdateAllUserRefsList(int $fromUserId, int $toUserId): array
{
$log = [];
// Tables with composite UNIQUE/PK (pre-dedup)
$composite = [
// table, column, uniqueOn (exclude the user column)
['access_url_rel_user', 'user_id', ['access_url_id']],
['fos_user_user_group', 'user_id', ['group_id']],
['user_rel_tag', 'user_id', ['tag_id']],
['user_rel_course_vote', 'user_id', ['c_id']],
['session_rel_user', 'user_id', ['session_id']],
['session_rel_course_rel_user', 'user_id', ['session_id', 'c_id']],
];
foreach ($composite as [$t,$col,$uniqueOn]) {
$n = self::duSafeUpdateUserRef($t, $col, $fromUserId, $toUserId, $uniqueOn);
if ($n) {
$log[] = "$t.$col ($n)";
}
}
// Simple tables (no composite unique with user_id)
$simple = [
['admin', 'user_id'],
['agenda_event_invitation', 'creator_id'],
['agenda_event_invitee', 'user_id'],
['class_user', 'user_id'],
['course_rel_user', 'user_id'],
['course_rel_user_catalogue', 'user_id'],
['course_request', 'user_id'],
['c_attendance_result', 'user_id'],
['c_attendance_sheet', 'user_id'],
['c_attendance_sheet_log', 'lastedit_user_id'],
['c_blog_comment', 'author_id'],
['c_blog_post', 'author_id'],
['c_blog_rating', 'user_id'],
['c_blog_rel_user', 'user_id'],
['c_blog_task_rel_user', 'user_id'],
['c_chat_connected', 'user_id'],
['c_dropbox_category', 'user_id'],
['c_dropbox_feedback', 'author_user_id'],
['c_dropbox_person', 'user_id'],
['c_dropbox_post', 'dest_user_id'],
['c_forum_mailcue', 'user_id'],
['c_forum_notification', 'user_id'],
['c_forum_post', 'poster_id'],
['c_forum_thread', 'thread_poster_id'],
['c_forum_thread_qualify', 'user_id'],
['c_forum_thread_qualify', 'qualify_user_id'],
['c_forum_thread_qualify_log', 'user_id'],
['c_forum_thread_qualify_log', 'qualify_user_id'],
['c_group_rel_tutor', 'user_id'],
['c_group_rel_user', 'user_id'],
['c_item_property', 'to_user_id'],
['c_item_property', 'insert_user_id'],
['c_item_property', 'lastedit_user_id'],
['c_lp_category_user', 'user_id'],
['c_lp_view', 'user_id'],
['c_notebook', 'user_id'],
['c_online_connected', 'user_id'],
['c_permission_user', 'user_id'],
['c_role_user', 'user_id'],
['c_student_publication', 'user_id'],
['c_student_publication_comment', 'user_id'],
['c_student_publication_rel_user', 'user_id'],
['c_survey', 'author'],
['c_survey_answer', 'user'],
['c_survey_invitation', 'user'],
['c_userinfo_content', 'user_id'],
['c_wiki', 'user_id'],
['c_wiki_mailcue', 'user_id'],
['chat', 'from_user'],
['chat', 'to_user'],
['chat_video', 'from_user'],
['chat_video', 'to_user'],
['extra_field_saved_search', 'user_id'],
['gradebook_category', 'user_id'],
['gradebook_certificate', 'user_id'],
['gradebook_evaluation', 'user_id'],
['gradebook_link', 'user_id'],
['gradebook_linkeval_log', 'user_id_log'],
['gradebook_result', 'user_id'],
['gradebook_result_log', 'user_id'],
['gradebook_score_log', 'user_id'],
['justification_document_rel_users', 'user_id'],
['message', 'user_receiver_id'],
['message', 'user_sender_id'],
['notification', 'dest_user_id'],
['personal_agenda', 'user'],
['plugin_bbb_meeting', 'user_id'],
['plugin_bbb_room', 'participant_id'],
['plugin_buycourses_item_rel_beneficiary', 'user_id'],
['plugin_buycourses_paypal_payouts', 'user_id'],
['plugin_buycourses_sale', 'user_id'],
['plugin_buycourses_subscription_rel_sale', 'user_id'],
['plugin_card_game', 'user_id'],
['plugin_h5p', 'user_id'],
['plugin_h5p_import_results', 'user_id'],
['plugin_lti_provider_result', 'user_id'],
['portfolio', 'user_id'],
['portfolio_category', 'user_id'],
['portfolio_comment', 'author_id'],
['sequence_value', 'user_id'],
['session', 'id_coach'],
['session', 'session_admin_id'],
['shared_survey', 'author'],
['skill_rel_user', 'argumentation_author_id'],
['skill_rel_user', 'assigned_by'],
['skill_rel_user', 'user_id'],
['skill_rel_user_comment', 'feedback_giver_id'],
['templates', 'user_id'],
['ticket_assigned_log', 'user_id'],
['ticket_assigned_log', 'sys_insert_user_id'],
['ticket_category', 'sys_insert_user_id'],
['ticket_category', 'sys_lastedit_user_id'],
['ticket_category_rel_user', 'user_id'],
['ticket_message', 'sys_insert_user_id'],
['ticket_message', 'sys_lastedit_user_id'],
['ticket_message_attachments', 'sys_lastedit_user_id'],
['ticket_message_attachments', 'sys_insert_user_id'],
['ticket_priority', 'sys_lastedit_user_id'],
['ticket_priority', 'sys_insert_user_id'],
['ticket_project', 'sys_lastedit_user_id'],
['ticket_project', 'sys_insert_user_id'],
['ticket_ticket', 'sys_lastedit_user_id'],
['ticket_ticket', 'sys_insert_user_id'],
['track_e_access', 'access_user_id'],
['track_e_access_complete', 'user_id'],
['track_e_attempt', 'user_id'],
['track_e_attempt_recording', 'author'],
['track_e_course_access', 'user_id'],
['track_e_downloads', 'down_user_id'],
['track_e_exercises', 'exe_user_id'],
['track_e_hotpotatoes', 'exe_user_id'],
['track_e_hotspot', 'hotspot_user_id'],
['track_e_item_property', 'lastedit_user_id'],
['track_e_lastaccess', 'access_user_id'],
['track_e_links', 'links_user_id'],
['track_e_login', 'login_user_id'],
['track_e_online', 'login_user_id'],
['track_e_uploads', 'upload_user_id'],
['track_stored_values', 'user_id'],
['track_stored_values_stack', 'user_id'],
['user', 'creator_id'],
['usergroup_rel_user', 'user_id'],
['user_api_key', 'user_id'],
['user_course_category', 'user_id'],
['user_rel_event_type', 'user_id'],
['xapi_internal_log', 'user_id'],
];
foreach ($simple as [$t,$col]) {
$n = self::duSafeUpdateUserRef($t, $col, $fromUserId, $toUserId);
if ($n) {
$log[] = "$t.$col ($n)";
}
}
// user_rel_user (two user columns)
if (self::duTableExists('user_rel_user')) {
// Dedup by user_id
$sqlDel1 = "
DELETE a FROM user_rel_user a
JOIN user_rel_user b
ON a.friend_user_id = b.friend_user_id
AND b.user_id = $toUserId
WHERE a.user_id = $fromUserId
";
Database::query($sqlDel1);
$resUp1 = Database::query("UPDATE IGNORE user_rel_user SET user_id = $toUserId WHERE user_id = $fromUserId");
$n1 = Database::affected_rows($resUp1);
// Dedup by friend_user_id
$sqlDel2 = "
DELETE a FROM user_rel_user a
JOIN user_rel_user b
ON a.user_id = b.user_id
AND b.friend_user_id = $toUserId
WHERE a.friend_user_id = $fromUserId
";
Database::query($sqlDel2);
$resUp2 = Database::query("UPDATE IGNORE user_rel_user SET friend_user_id = $toUserId WHERE friend_user_id = $fromUserId");
$n2 = Database::affected_rows($resUp2);
if ($n1) {
$log[] = "user_rel_user.user_id ($n1)";
}
if ($n2) {
$log[] = "user_rel_user.friend_user_id ($n2)";
}
}
// Special cases
$nDef = self::duUpdateTrackEDefault($fromUserId, $toUserId);
if ($nDef) {
$log[] = "track_e_default (default_user_id/default_value) ($nDef)";
}
$nEf = self::duUpdateExtraFieldValuesUserType($fromUserId, $toUserId); // idem
if ($nEf) {
$log[] = "extra_field_values.item_id (user-type) ($nEf)";
}
return $log;
}
/**
* Gets a list of users who were enrolled in the lessons.
* It is necessary that in the extra field, a company is defined.
@@ -4569,4 +5004,83 @@ class MySpace
return $sql;
}
/** track_e_default: default_value text with user_id in various formats and numeric default_user_id. */
private static function duUpdateTrackEDefault(int $fromUserId, int $toUserId): int
{
if (!self::duTableExists('track_e_default')) {
return 0;
}
$moved = 0;
// default_user_id (if present)
if (self::duColumnExists('track_e_default', 'default_user_id')) {
$result1 = Database::query("UPDATE IGNORE track_e_default SET default_user_id = $toUserId WHERE default_user_id = $fromUserId");
$moved += Database::affected_rows($result1);
}
// Events where default_value == user id as plain text
$eventsDirect = [
'user_created', 'exe_result_deleted', 'session_add_user_course', 'session_add_user',
'user_updated', 'user_deleted', 'exe_attempt_deleted', 'user_disable', 'user_password_updated',
'user_enable', 'session_delete_user', 'session_delete_user_course', 'exe_incomplete_results_deleted',
];
if (self::duColumnExists('track_e_default', 'event_type') && self::duColumnExists('track_e_default', 'default_value')) {
$in = "'".implode("','", array_map('Database::escape_string', $eventsDirect))."'";
$sql = "
UPDATE track_e_default
SET default_value = '$toUserId'
WHERE default_value = '$fromUserId'
AND event_type IN ($in)
";
$result2 = Database::query($sql);
$moved += Database::affected_rows($result2);
}
// user_subscribed / user_unsubscribed with default_value_type='user_object'
if (self::duColumnExists('track_e_default', 'default_value_type') && self::duColumnExists('track_e_default', 'event_type')) {
foreach (['user_subscribed', 'user_unsubscribed'] as $evt) {
// Replace twice the ID and also the profile.php?u=FROM
$moved += self::duReplaceInColumn('track_e_default', 'default_value', "profile.php?u=".$fromUserId, "profile.php?u=".$toUserId);
$moved += self::duReplaceInColumn('track_e_default', 'default_value', (string) $fromUserId, (string) $toUserId);
// NOTE: REPLACE already uses LIKE; we do not narrow with WHERE to keep cross-install compatibility.
}
}
// soc_gr_u_unsubs / soc_gr_u_subs: "gid: GGG - uid: XXX"
foreach (['soc_gr_u_unsubs', 'soc_gr_u_subs'] as $evt) {
$moved += self::duReplaceInColumn('track_e_default', 'default_value', "uid: ".$fromUserId, "uid: ".$toUserId);
}
return $moved;
}
/** extra_field_values: for extra_field.extra_field_type = 1 (user), move item_id. */
private static function duUpdateExtraFieldValuesUserType(int $fromUserId, int $toUserId): int
{
if (!self::duTableExists('extra_field') || !self::duTableExists('extra_field_values')) {
return 0;
}
// Remove collisions (same field_id already pointing to target user)
$sqlDel = "
DELETE v
FROM extra_field_values v
JOIN extra_field f ON f.id = v.field_id AND f.extra_field_type = 1
JOIN extra_field_values w ON w.field_id = v.field_id AND w.item_id = $toUserId
WHERE v.item_id = $fromUserId
";
Database::query($sqlDel);
// Move remaining
$sqlUpd = "
UPDATE extra_field_values v
JOIN extra_field f ON f.id = v.field_id AND f.extra_field_type = 1
SET v.item_id = $toUserId
WHERE v.item_id = $fromUserId
";
$result = Database::query($sqlUpd);
return Database::affected_rows($result);
}
}
+1 -1
View File
@@ -861,7 +861,7 @@ class nusoap_base
$sec = time();
$usec = 0;
}
return strftime('%Y-%m-%d %H:%M:%S', $sec) . '.' . sprintf('%06d', $usec);
return date('Y-m-d H:i:s', $sec) . '.' . sprintf('%06d', $usec);
}
/**
+5 -2
View File
@@ -52,11 +52,14 @@ class OpenGraph implements Iterator
curl_setopt($curl, CURLOPT_FAILONERROR, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_MAXREDIRS, 3);
curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_TIMEOUT, 15);
curl_setopt($curl, CURLOPT_TIMEOUT, 10);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
curl_setopt($curl, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT'] ?? 'Chamilo');
$response = curl_exec($curl);
@@ -38,10 +38,8 @@ class HTML_QuickForm_Rule_CompareDate extends HTML_QuickForm_Rule
if (!is_array($values[0]) && !is_array($values[1])) {
return api_strtotime($values[0]) < api_strtotime($values[1]);
} else {
$compareFn = create_function(
'$a, $b', 'return mktime($a[\'H\'],$a[\'i\'],0,$a[\'M\'],$a[\'d\'],$a[\'Y\']) <= mktime($b[\'H\'],$b[\'i\'],0,$b[\'M\'],$b[\'d\'],$b[\'Y\'] );'
);
return $compareFn($values[0], $values[1]);
return mktime($values[0]['H'], $values[0]['i'], 0, $values[0]['M'], $values[0]['d'], $values[0]['Y'])
<= mktime($values[1]['H'], $values[1]['i'], 0, $values[1]['M'], $values[1]['d'], $values[1]['Y']);
}
}
}
@@ -14,7 +14,6 @@ class Html_Quickform_Rule_Date extends HTML_QuickForm_Rule
*/
function validate($date, $options)
{
$compareDate = create_function('$a', 'return checkdate($a[\'M\'],$a[\'d\'],$a[\'Y\']);');
return $compareDate($date);
return checkdate($date['M'], $date['d'], $date['Y']);
}
}
@@ -773,14 +773,6 @@ class HTML_QuickForm_advmultiselect extends HTML_QuickForm_select
$leftAll = "<br /><br /><button $attrStrAdd /> <em class='fa fa-backward'></em></button>";
}
$strHtmlAdd = "<div class='d-flex justify-content-center align-items-center' style='height: 100%; padding-top: 100px;'>
<button $attrStrAdd><em class='fa fa-arrow-right'></em></button>
</div>";
$strHtmlRemove = "<div class='d-flex justify-content-center align-items-center' style='padding-top: 10px;'>
<button $attrStrRemove><em class='fa fa-arrow-left'></em></button>
</div>";
// build the select all button with all its attributes
$strHtmlAll = '';

Some files were not shown because too many files have changed in this diff Show More