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
@@ -312,6 +312,8 @@ class AzureActiveDirectory extends Plugin
$extra,
] = $this->formatUserData($azureUserInfo);
$userInfo = api_get_user_info($userId);
$userId = UserManager::update_user(
$userId,
$firstNme,
@@ -323,7 +325,7 @@ class AzureActiveDirectory extends Plugin
STUDENT,
null,
$phone,
null,
$userInfo['picture_uri'],
null,
$active,
null,
+3 -1
View File
@@ -73,7 +73,9 @@ class BigBlueButtonBN
curl_close( $ch );
if ($data) {
return (new SimpleXMLElement($data));
$xml = simplexml_load_string($data, SimpleXMLElement::class, LIBXML_NONET);
return $xml !== false ? $xml : false;
} else {
return false;
}
@@ -8,6 +8,8 @@ $course_plugin = 'customcertificate';
require_once __DIR__.'/../config.php';
api_block_anonymous_users();
/** @var CustomCertificatePlugin $plugin */
$plugin = CustomCertificatePlugin::create();
$enable = $plugin->get('enable_plugin_customcertificate') === 'true';
$tblProperty = Database::get_course_table(TABLE_ITEM_PROPERTY);
@@ -27,18 +29,63 @@ if (!$enable) {
$currentLocalTime = api_get_local_time();
$accessUrlId = api_get_current_access_url_id();
$sessionId = isset($_GET['session_id']) ? (int) $_GET['session_id'] : null;
$dateBegin = isset($_GET['date_begin']) ? strtotime($_GET['date_begin']) : null;
$dateEnd = isset($_GET['date_end']) ? strtotime($_GET['date_end'].' 23:59:59') : null;
/*
* Support single or multiple session_id parameters:
* - session_id=3
* - session_id[0]=3&session_id[1]=5
*/
$sessionIds = [];
if (isset($_GET['session_id'])) {
if (is_array($_GET['session_id'])) {
foreach ($_GET['session_id'] as $rawId) {
$id = (int) $rawId;
if ($id > 0) {
$sessionIds[] = $id;
}
}
} else {
$id = (int) $_GET['session_id'];
if ($id > 0) {
$sessionIds[] = $id;
}
}
}
if (api_is_multiple_url_enabled()) {
if ($accessUrlId != -1) {
// Remove duplicates just in case.
$sessionIds = array_values(array_unique($sessionIds));
if (empty($sessionIds)) {
// No session selected: nothing to export.
Display::display_header($plugin->get_lang('PrintCertificate'));
echo Display::return_message(get_lang('NoResultsAvailable'), 'warning');
Display::display_footer();
exit;
}
// Date filters (same logic as session_filter.php)
$dateBeginRaw = isset($_GET['date_begin']) ? $_GET['date_begin'] : null;
$dateEndRaw = isset($_GET['date_end']) ? $_GET['date_end'] : null;
$dateBegin = !empty($dateBeginRaw) ? strtotime($dateBeginRaw) : null;
$dateEnd = !empty($dateEndRaw) ? strtotime($dateEndRaw.' 23:59:59') : null;
$filterDate = 0;
if (!empty($dateBegin)) {
$filterDate += DATE_BEGIN_FILTER;
}
if (!empty($dateEnd)) {
$filterDate += DATE_END_FILTER;
}
// Multi-URL protection: every session must belong to the current URL.
if (api_is_multiple_url_enabled() && $accessUrlId != -1) {
foreach ($sessionIds as $sessionId) {
$result = Database::select(
'*',
"$tblSessionRelAccessUrl",
$tblSessionRelAccessUrl,
[
'where' => [
"access_url_id = ? AND session_id = ?" => [$accessUrlId, $sessionId],
'access_url_id = ? AND session_id = ?' => [$accessUrlId, $sessionId],
],
]
);
@@ -52,51 +99,42 @@ if (api_is_multiple_url_enabled()) {
$exportAllInOne = isset($_GET['export_pdf']) ? (int) $_GET['export_pdf'] : false;
$exportZip = isset($_GET['export_zip']) ? (int) $_GET['export_zip'] : false;
$filterDate = 0;
if (!empty($dateBegin)) {
$filterDate += DATE_BEGIN_FILTER;
}
if (!empty($dateEnd)) {
$filterDate += DATE_END_FILTER;
}
// Build extra fields filter (same approach as session_filter.php).
$filterCheckList = [];
$extraField = new ExtraField('user');
$extraFieldsAll = $extraField->get_all(['filter = ?' => 1], 'option_order');
foreach ($extraFieldsAll as $field) {
if (!empty($_GET['extra_'.$field['variable']])) {
$paramName = 'extra_'.$field['variable'];
if (!empty($_GET[$paramName])) {
$filterCheckList[$field['id']] = $field;
}
}
$result = Database::select(
'c.id, c.code',
"$tblCourse c INNER JOIN $tblSessionRelCourse r ON c.id = r.c_id",
[
'where' => [
"r.session_id = ? " => [$sessionId],
],
]
);
// Collect all certificates from all selected sessions and their courses (same logic as session_filter.php).
$certificateList = [];
foreach ($result as $value) {
$courseId = $value['id'];
$courseCode = $value['code'];
$cats = Category::load(
null,
null,
$courseCode,
null,
null,
$sessionId,
'ORDER BY id'
foreach ($sessionIds as $sessionId) {
$courseResult = Database::select(
'c.id, c.code',
"$tblCourse c INNER JOIN $tblSessionRelCourse r ON c.id = r.c_id",
[
'where' => [
'r.session_id = ? ' => [$sessionId],
],
]
);
if (empty($cats)) {
// first time
if (empty($courseResult)) {
continue;
}
foreach ($courseResult as $value) {
$courseId = (int) $value['id'];
$courseCode = $value['code'];
// Load gradebook categories for this course + session.
$cats = Category::load(
0,
null,
null,
$courseCode,
null,
@@ -104,40 +142,72 @@ foreach ($result as $value) {
$sessionId,
'ORDER BY id'
);
}
$selectCat = (int) $cats[0]->get_id();
$certificateList = [];
$certificateListAux = GradebookUtils::get_list_users_certificates($selectCat);
if (empty($cats)) {
// First time, try with default category id = 0 (same as session_filter.php).
$cats = Category::load(
0,
null,
$courseCode,
null,
null,
$sessionId,
'ORDER BY id'
);
}
foreach ($certificateListAux as $value) {
$created_at = strtotime(api_get_local_time($value['created_at']));
$value['category_id'] = $selectCat;
$value['c_id'] = $courseId;
$value['course_code'] = $courseCode;
switch ($filterDate) {
case NO_DATE_FILTER:
$certificateList[] = $value;
break;
case DATE_BEGIN_FILTER:
if ($created_at >= $dateBegin) {
$certificateList[] = $value;
}
break;
case DATE_END_FILTER:
if ($created_at <= $dateEnd) {
$certificateList[] = $value;
}
break;
case ALL_DATE_FILTER:
if ($created_at >= $dateBegin && $created_at <= $dateEnd) {
$certificateList[] = $value;
}
break;
if (empty($cats)) {
// No gradebook category for this course/session.
continue;
}
$selectCat = (int) $cats[0]->get_id();
if ($selectCat <= 0) {
continue;
}
// Get all certificates for this category.
$certificateListAux = GradebookUtils::get_list_users_certificates($selectCat);
if (empty($certificateListAux)) {
continue;
}
foreach ($certificateListAux as $certRow) {
$createdAt = strtotime(api_get_local_time($certRow['created_at']));
// Base row with extra metadata.
$row = $certRow;
$row['category_id'] = $selectCat;
$row['c_id'] = $courseId;
$row['course_code'] = $courseCode;
$row['session_id'] = $sessionId;
// Apply date filter (same logic as session_filter.php).
$include = false;
switch ($filterDate) {
case NO_DATE_FILTER:
$include = true;
break;
case DATE_BEGIN_FILTER:
$include = $createdAt >= $dateBegin;
break;
case DATE_END_FILTER:
$include = $createdAt <= $dateEnd;
break;
case ALL_DATE_FILTER:
$include = $createdAt >= $dateBegin && $createdAt <= $dateEnd;
break;
}
if ($include) {
$certificateList[] = $row;
}
}
}
}
// Filter extra field
// Apply extra fields filtering to the global certificate list (same idea as session_filter.php).
if (!empty($filterCheckList) && !empty($certificateList)) {
foreach ($certificateList as $key => $value) {
foreach ($filterCheckList as $fieldId => $field) {
$extraFieldValue = new ExtraFieldValue('user');
@@ -151,42 +221,73 @@ foreach ($result as $value) {
break;
}
$paramName = 'extra_'.$field['variable'];
switch ($field['field_type']) {
case ExtraField::FIELD_TYPE_TEXT:
case ExtraField::FIELD_TYPE_ALPHANUMERIC:
$pos = stripos($extraFieldValueData['value'], $_GET['extra_'.$field['variable']]);
if ($pos === false) {
unset($certificateList[$key]);
$filterValue = isset($_GET[$paramName]) ? (string) $_GET[$paramName] : '';
if ($filterValue !== '') {
$pos = stripos($extraFieldValueData['value'], $filterValue);
if ($pos === false) {
unset($certificateList[$key]);
}
}
break;
case ExtraField::FIELD_TYPE_RADIO:
$valueRadio = $_GET['extra_'.$field['variable']]['extra_'.$field['variable']];
if ($extraFieldValueData['value'] != $valueRadio) {
$filterValue = '';
if (isset($_GET[$paramName][$paramName])) {
$filterValue = $_GET[$paramName][$paramName];
}
if ($extraFieldValueData['value'] != $filterValue) {
unset($certificateList[$key]);
}
break;
case ExtraField::FIELD_TYPE_SELECT:
if ($extraFieldValueData['value'] != $_GET['extra_'.$field['variable']]) {
$filterValue = isset($_GET[$paramName]) ? $_GET[$paramName] : null;
if ($filterValue !== null && $extraFieldValueData['value'] != $filterValue) {
unset($certificateList[$key]);
}
break;
}
// Stop checking other fields if this one already disqualified the record.
if (!isset($certificateList[$key])) {
break;
}
}
}
}
// Re-index the array after unsetting elements.
$certificateList = array_values($certificateList);
// If no certificates pass the filters, show a friendly message.
if (empty($certificateList)) {
Display::display_header($plugin->get_lang('PrintCertificate'));
echo Display::return_message(get_lang('NoResultsAvailable'), 'warning');
Display::display_footer();
exit;
}
// Build user list + session mapping.
$userList = [];
foreach ($certificateList as $index => $value) {
$sessionInfoPerSession = [];
foreach ($certificateList as $value) {
$infoUser = api_get_user_info($value['user_id']);
$infoUser['category_id'] = $value['category_id'];
$infoUser['c_id'] = $value['c_id'];
$infoUser['course_code'] = $value['course_code'];
$infoUser['session_id'] = $value['session_id'];
$userList[] = $infoUser;
}
$sessionInfo = [];
if ($sessionId > 0) {
$sessionInfo = SessionManager::fetch($sessionId);
// Preload session info for all involved sessions (used in dates).
foreach ($sessionIds as $sessionId) {
if ($sessionId > 0) {
$sessionInfoPerSession[$sessionId] = SessionManager::fetch($sessionId);
}
}
$path = api_get_path(WEB_UPLOAD_PATH).'certificates/';
@@ -196,14 +297,17 @@ foreach ($userList as $userInfo) {
$courseId = $userInfo['c_id'];
$courseCode = $userInfo['course_code'];
$studentId = $userInfo['user_id'];
$sessionId = !empty($userInfo['session_id']) ? (int) $userInfo['session_id'] : 0;
$courseInfo = api_get_course_info($courseCode);
$allowCustomCertificate = api_get_course_setting('customcertificate_course_enable', $courseInfo);
if (!$allowCustomCertificate) {
$allowCustomCertificateCourse = api_get_course_setting('customcertificate_course_enable', $courseInfo);
if (!$allowCustomCertificateCourse) {
// Skip courses where the custom certificate plugin is not enabled at course level.
continue;
}
// Get info certificate
// Get info certificate for this course + session.
$infoCertificate = CustomCertificatePlugin::getInfoCertificate($courseId, $sessionId, $accessUrlId);
if (!is_array($infoCertificate)) {
@@ -221,8 +325,8 @@ foreach ($userList as $userInfo) {
}
}
$workSpace = intval(297 - $infoCertificate['margin_left'] - $infoCertificate['margin_right']);
$widthCell = intval($workSpace / 6);
$workSpace = (int) (297 - $infoCertificate['margin_left'] - $infoCertificate['margin_right']);
$widthCell = (int) ($workSpace / 6);
$htmlText = '';
if (!$exportAllInOne) {
@@ -237,7 +341,6 @@ foreach ($userList as $userInfo) {
href="'.api_get_path(WEB_CSS_PATH).'document.css">';
$htmlText .= '<body>';
}
$studentId = $userInfo['user_id'];
if (empty($infoCertificate['background'])) {
$htmlText .= '<div class="caraA" style="page-break-before:always; margin:0px; padding:0px;">';
@@ -262,7 +365,7 @@ foreach ($userList as $userInfo) {
if (!empty($infoCertificate['logo_center'])) {
$logoCenter = '
<img
style="max-height: 150px; max-width: '.intval($workSpace - (2 * $widthCell)).'mm;"
style="max-height: 150px; max-width: '.(int) ($workSpace - (2 * $widthCell)).'mm;"
src="'.$path.$infoCertificate['logo_center'].'" />';
}
@@ -282,11 +385,11 @@ foreach ($userList as $userInfo) {
"
border="0">';
$htmlText .= '<tr>';
$htmlText .= '<td style="width:'.intval($workSpace / 3).'mm" class="logo">'.$logoLeft.'</td>';
$htmlText .= '<td style="width:'.intval($workSpace / 3).'mm; text-align:center;" class="logo">';
$htmlText .= '<td style="width:'.(int) ($workSpace / 3).'mm" class="logo">'.$logoLeft.'</td>';
$htmlText .= '<td style="width:'.(int) ($workSpace / 3).'mm; text-align:center;" class="logo">';
$htmlText .= $logoCenter;
$htmlText .= '</td>';
$htmlText .= '<td style="width:'.intval($workSpace / 3).'mm; text-align:right;" class="logo">'.$logoRight.'</td>';
$htmlText .= '<td style="width:'.(int) ($workSpace / 3).'mm; text-align:right;" class="logo">'.$logoRight.'</td>';
$htmlText .= '</tr>';
$htmlText .= '</table>';
@@ -307,15 +410,18 @@ foreach ($userList as $userInfo) {
$myContentHtml
);
// Use session-specific dates when available.
$currentSessionInfo = isset($sessionInfoPerSession[$sessionId]) ? $sessionInfoPerSession[$sessionId] : [];
$startDate = '';
$endDate = '';
switch ($infoCertificate['date_change']) {
case 0:
if (!empty($sessionInfo['access_start_date'])) {
$startDate = date("d/m/Y", strtotime(api_get_local_time($sessionInfo['access_start_date'])));
if (!empty($currentSessionInfo['access_start_date'])) {
$startDate = date("d/m/Y", strtotime(api_get_local_time($currentSessionInfo['access_start_date'])));
}
if (!empty($sessionInfo['access_end_date'])) {
$endDate = date("d/m/Y", strtotime(api_get_local_time($sessionInfo['access_end_date'])));
if (!empty($currentSessionInfo['access_end_date'])) {
$endDate = date("d/m/Y", strtotime(api_get_local_time($currentSessionInfo['access_end_date'])));
}
break;
case 1:
@@ -364,8 +470,8 @@ foreach ($userList as $userInfo) {
} elseif ($infoCertificate['type_date_expediction'] == 4) {
$dateExpediction .= $plugin->get_lang('to').$infoToReplaceInContentHtml[9]; //date_certificate_no_time
} else {
if (!empty($sessionInfo)) {
$dateInfo = api_get_local_time($sessionInfo['access_end_date']);
if (!empty($currentSessionInfo)) {
$dateInfo = api_get_local_time($currentSessionInfo['access_end_date']);
$dateExpediction .= $plugin->get_lang('to').api_format_date($dateInfo, DATE_FORMAT_LONG);
}
}
@@ -571,12 +677,12 @@ foreach ($userList as $userInfo) {
$htmlText .= '<table width="100%" class="contents-learnpath">';
$htmlText .= '<tr>';
$htmlText .= '<td>';
$myContentHtml = strip_tags(
$myContentHtmlBack = strip_tags(
$infoCertificate['contents'],
'<p><b><strong><table><tr><td><th><span><i><li><ol><ul>'.
'<dd><dt><dl><br><hr><img><a><div><h1><h2><h3><h4><h5><h6>'
);
$htmlText .= $myContentHtml;
$htmlText .= $myContentHtmlBack;
$htmlText .= '</td>';
$htmlText .= '</tr>';
$htmlText .= '</table>';
@@ -591,6 +697,15 @@ foreach ($userList as $userInfo) {
$htmlList[$fileName] = $htmlText;
}
// If for some reason we ended up with no HTML (e.g. all courses had the plugin disabled),
// show a message instead of a blank page.
if (empty($htmlList)) {
Display::display_header($plugin->get_lang('PrintCertificate'));
echo Display::return_message(get_lang('NoResultsAvailable'), 'warning');
Display::display_footer();
exit;
}
$fileList = [];
$archivePath = api_get_path(SYS_ARCHIVE_PATH).'certificates/';
if (!is_dir($archivePath)) {
+1 -1
View File
@@ -51,7 +51,7 @@ if ($enableCourse && $useDefault) {
api_not_allowed(true, $plugin->get_lang('ToolUseDefaultSettingCourse'));
}
$allow = api_is_platform_admin() || api_is_teacher();
$allow = api_is_platform_admin() || api_is_teacher() || api_is_allowed_to_session_edit();
if (!$allow) {
api_not_allowed(true);
}
+1 -1
View File
@@ -11,7 +11,7 @@ $plugin = CustomCertificatePlugin::create();
$enable = $plugin->get('enable_plugin_customcertificate') == 'true';
if ($enable) {
if (api_is_platform_admin() || api_is_teacher()) {
if (api_is_platform_admin() || api_is_teacher() || api_is_allowed_to_session_edit()) {
$url = 'src/index.php?';
$url .= (isset($_GET['cidReq']) ? api_get_cidreq() : 'default=1');
header('Location: '.$url);
+172 -19
View File
@@ -64,6 +64,45 @@ class ZipPackageImporter extends H5pPackageImporter
'css',
];
/**
* Extensions that must never be extracted, regardless of allowlist.
* These can enable server-side code execution.
*/
private const BLOCKED_EXTENSIONS = [
'php',
'php3',
'php4',
'php5',
'php6',
'php7',
'phtml',
'phar',
'shtml',
'cgi',
'pl',
'py',
'rb',
'sh',
'bash',
'bat',
'cmd',
'exe',
'dll',
'so',
];
/**
* Filenames that must never be extracted.
* These can override server configuration to enable code execution.
*/
private const BLOCKED_FILENAMES = [
'.htaccess',
'.htpasswd',
'.user.ini',
'web.config',
'php.ini',
];
/**
* Import an H5P package. No DB change.
*
@@ -89,7 +128,18 @@ class ZipPackageImporter extends H5pPackageImporter
$pathInfo = pathinfo($this->packageFileInfo['name']);
$packageDirectoryPath = $this->generatePackageDirectory($pathInfo['filename']);
$zipFile->extract($packageDirectoryPath);
// Extract only the files that passed validation — never extract the full
// archive blindly, as the zip may contain entries skipped during validation.
$safeFiles = $this->getSafeFileList($zipContent);
$zipFile->extract(
PCLZIP_OPT_PATH, $packageDirectoryPath,
PCLZIP_OPT_BY_NAME, $safeFiles
);
// Write a protective .htaccess so that even if a server misconfiguration
// exists, files in this directory cannot be executed as scripts.
$this->writeProtectiveHtaccess($packageDirectoryPath);
return "{$packageDirectoryPath}";
}
@@ -111,8 +161,17 @@ class ZipPackageImporter extends H5pPackageImporter
/**
* Validate an H5P package.
* Check if 'h5p.json' or 'content/content.json' files exist
* and if the files are in a file whitelist (ALLOWED_EXTENSIONS).
*
* Every entry in the zip must pass all of the following checks before
* extraction is allowed:
* - No file or directory component may start with '.' or '_' (blocks
* .htaccess, .htpasswd, .user.ini, etc.)
* - The base filename must not be in the blocked-filenames list.
* - The extension must not be in the blocked-extensions list.
* - The extension must be in the allowed-extensions list.
*
* Additionally the archive must contain 'h5p.json' to be considered a
* valid H5P package.
*
* @param array $h5pPackageContent the content of the H5P package
*
@@ -120,29 +179,123 @@ class ZipPackageImporter extends H5pPackageImporter
*/
private function validateH5pPackageContent(array $h5pPackageContent): bool
{
$validPackage = false;
if (empty($h5pPackageContent)) {
return false;
}
if (!empty($h5pPackageContent)) {
foreach ($h5pPackageContent as $content) {
$filename = $content['filename'];
$hasH5pJson = false;
if (0 !== preg_match('/(^[\._]|\/[\._]|\\\[\._])/', $filename)) {
// Skip any file or folder starting with a . or _
continue;
}
foreach ($h5pPackageContent as $content) {
$filename = $content['filename'];
$fileExtension = pathinfo($filename, PATHINFO_EXTENSION);
// Reject — do NOT skip — any file or directory component that starts
// with '.' or '_'. Previously this used `continue`, which allowed
// dangerous files like .htaccess to be silently included in the
// extraction while the loop kept searching for h5p.json.
if (0 !== preg_match('/(^[\._]|\/[\._]|\\\[\._])/', $filename)) {
return false;
}
if (in_array($fileExtension, self::ALLOWED_EXTENSIONS)) {
$validPackage = 'h5p.json' === $filename || 'content/content.json' === $filename;
if ($validPackage) {
break;
}
}
// Directories have no extension to check; skip the extension tests.
if (1 === ($content['folder'] ?? 0)) {
continue;
}
$basename = basename($filename);
$fileExtension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
// Reject server-configuration and script-execution override files.
if (in_array(strtolower($basename), self::BLOCKED_FILENAMES, true)) {
return false;
}
// Reject extensions that can result in server-side code execution.
if (in_array($fileExtension, self::BLOCKED_EXTENSIONS, true)) {
return false;
}
// Reject any extension not explicitly on the allowlist.
if (!in_array($fileExtension, self::ALLOWED_EXTENSIONS, true)) {
return false;
}
if ('h5p.json' === $filename) {
$hasH5pJson = true;
}
}
return $validPackage;
return $hasH5pJson;
}
/**
* Return filenames of zip entries that are safe to extract.
*
* This mirrors the security checks in validateH5pPackageContent so that
* the extraction step is independently guarded even if validation logic
* evolves in the future.
*
* @param array $h5pPackageContent PclZip listContent() result
*
* @return array list of filenames safe to pass to PCLZIP_OPT_BY_NAME
*/
private function getSafeFileList(array $h5pPackageContent): array
{
$safeFiles = [];
foreach ($h5pPackageContent as $content) {
$filename = $content['filename'];
if (0 !== preg_match('/(^[\._]|\/[\._]|\\\[\._])/', $filename)) {
continue;
}
if (1 === ($content['folder'] ?? 0)) {
continue;
}
$basename = basename($filename);
$fileExtension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (in_array(strtolower($basename), self::BLOCKED_FILENAMES, true)) {
continue;
}
if (in_array($fileExtension, self::BLOCKED_EXTENSIONS, true)) {
continue;
}
if (!in_array($fileExtension, self::ALLOWED_EXTENSIONS, true)) {
continue;
}
$safeFiles[] = $filename;
}
return $safeFiles;
}
/**
* Write a protective .htaccess into the extracted package directory.
*
* This is a defence-in-depth measure: even if a file with a dangerous
* extension somehow reached the directory (e.g. via a future code path),
* Apache will not execute it as a script and will not allow per-directory
* configuration files to override this setting.
*/
private function writeProtectiveHtaccess(string $directory): void
{
$htaccessPath = $directory.'/.htaccess';
$content = <<<'HTACCESS'
# Auto-generated by Chamilo H5P importer — do not remove.
# Prevent execution of any server-side scripts in this directory.
Options -ExecCGI -Indexes
php_flag engine off
RemoveHandler .php .php3 .php4 .php5 .php6 .php7 .phtml .phar
RemoveType .php .php3 .php4 .php5 .php6 .php7 .phtml .phar
AddType text/plain .php .php3 .php4 .php5 .php6 .php7 .phtml .phar .txt
HTACCESS;
file_put_contents($htaccessPath, $content);
}
private function generatePackageDirectory(string $name): string
+11 -2
View File
@@ -486,7 +486,13 @@ class ImsLtiPlugin extends Plugin
throw new Exception($this->get_lang('NoAccessToUrl'));
}
$xml = new SimpleXMLElement($content);
libxml_use_internal_errors(true);
$xml = simplexml_load_string($content, SimpleXMLElement::class, LIBXML_NONET);
if ($xml === false) {
throw new Exception($this->get_lang('LaunchUrlNotFound'));
}
$result = $xml->xpath('blti:launch_url');
if (empty($result)) {
@@ -718,7 +724,10 @@ class ImsLtiPlugin extends Plugin
return null;
}
return new SimpleXMLElement($request);
libxml_use_internal_errors(true);
$xml = simplexml_load_string($request, SimpleXMLElement::class, LIBXML_NONET);
return $xml !== false ? $xml : null;
}
/**
+1
View File
@@ -25,6 +25,7 @@ external tool.
# Installation
* Make sure PHP's [OpenSSL extension](https://www.php.net/manual/en/openssl.installation.php) is available on your server
* Prepare your web server to allow to send cookies in all contexts, set the `SameSite` attribute to `None`
* i.e. Apache configuration
+1
View File
@@ -75,3 +75,4 @@ $strings['ReplacementUserIdHelp'] = 'The current user_id (sub) in launch params
$strings['AddInCourses'] = 'Add in courses';
$strings['AddInAllCourses'] = 'Add in all courses';
$strings['AddInSessions'] = 'Add in sessions';
$strings["BaseToolsCanBeAddedInSessionsOnly"] = "Base tools can be added in sessions only";
+1
View File
@@ -75,3 +75,4 @@ $strings['ReplacementUserIdHelp'] = 'Le user_id (sub) actuel dans les paramètre
$strings['AddInCourses'] = 'Ajouter aux cours';
$strings['AddInAllCourses'] = 'Ajouter à tous les cours';
$strings['AddInSessions'] = 'Ajouter aux sessions';
$strings["BaseToolsCanBeAddedInSessionsOnly"] = "Les outils de base peuvent uniquement être ajoutés dans les sessions";
+1
View File
@@ -75,3 +75,4 @@ $strings['ReplacementUserIdHelp'] = 'El user_id (sub) actual en los parametros d
$strings['AddInCourses'] = 'Añadir a cursos';
$strings['AddInAllCourses'] = 'Aãndir a todos los cursos';
$strings['AddInSessions'] = 'Añadir a sesiones';
$strings["BaseToolsCanBeAddedInSessionsOnly"] = "Las herramientas base pueden ser añadidas únicamente en sesiones";
+3 -3
View File
@@ -32,13 +32,13 @@ try {
}
if ($tool->getParent()) {
throw new Exception($plugin->get_lang('NoAllowed'));
throw new Exception($plugin->get_lang('BaseToolsCanBeAddedInSessionsOnly'));
}
$session = api_get_session_entity($sessionId);
if (!$session) {
api_not_allowed(true);
throw new Exception(get_lang('Session not found.'));
}
$content = '';
@@ -71,7 +71,7 @@ try {
'multiple' => true,
]
);
$form->addCheckBox('tool_visible', get_lang('SetVisible'), get_lang('ToolIsNowVisible'));
$form->addCheckBox('tool_visible', get_lang('Set visible'), get_lang('The tool is now visible.'));
$form->addButtonExport(get_lang('Save'));
if ($form->validate()) {
+2 -2
View File
@@ -31,7 +31,7 @@ try {
}
if ($tool->getParent()) {
throw new Exception($plugin->get_lang('NoAllowed'));
throw new Exception($plugin->get_lang('BaseToolsCanBeAddedInSessionsOnly'));
}
$content = '';
@@ -60,7 +60,7 @@ try {
if (!$formValues['sessions']) {
Display::addFlash(
Display::return_message($plugin->get_lang('NeedToSelectASession'), 'error', false)
Display::return_message(get_lang('Session not found.'), 'error', false)
);
header('Location:'.api_get_self());
exit;
+4 -1
View File
@@ -540,6 +540,9 @@ class LtiProviderPlugin extends Plugin
return null;
}
return new SimpleXMLElement($request);
libxml_use_internal_errors(true);
$xml = simplexml_load_string($request, SimpleXMLElement::class, LIBXML_NONET);
return $xml !== false ? $xml : null;
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ use Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver;
use Doctrine\ORM\ORMException;
use Doctrine\ORM\Tools\SchemaTool;
use GuzzleHttp\RequestOptions;
use Http\Adapter\Guzzle6\Client;
use Http\Adapter\Guzzle7\Client;
use Http\Message\MessageFactory\GuzzleMessageFactory;
use Ramsey\Uuid\Uuid;
use Xabbuh\XApi\Client\Api\StatementsApiClientInterface;
+2 -1
View File
@@ -31,7 +31,8 @@ class JWTClient extends Client
'iss' => $apiKey,
'exp' => (time() + 60) * 1000, // will expire in one minute
],
$apiSecret
$apiSecret,
'HS256'
);
self::register($this);
}