Compare commits

...

6 Commits

  1. 24
      .htaccess
  2. 44
      documentation/changelog.html
  3. 1
      main/auth/external_login/facebook.inc.php
  4. 38
      main/auth/external_login/functions.inc.php
  5. 4
      main/exercise/export/aiken/aiken_import.inc.php
  6. 2
      main/inc/lib/Compilatio.php
  7. 2
      main/inc/lib/api.lib.php
  8. 3
      main/inc/lib/formvalidator/Element/HtmlEditor.php
  9. 7
      main/inc/lib/glossary.lib.php
  10. 2
      main/lp/LpAiHelper.php
  11. 4
      main/lp/learnpath.class.php
  12. 1
      main/session/resume_session.php
  13. 2
      main/template/default/exercise/submit.js.tpl
  14. 130
      plugin/ai_helper/AiHelperPlugin.php
  15. 12
      plugin/ai_helper/src/deepseek/DeepSeek.php
  16. 2
      plugin/ai_helper/src/deepseek/DeepSeekUrl.php
  17. 3
      plugin/ai_helper/tool/answers.php
  18. 1
      plugin/ai_helper/tool/learnpath.php
  19. 46
      plugin/azure_active_directory/src/AzureActiveDirectory.php
  20. 6
      plugin/azure_active_directory/src/Entity/AzureSyncState.php

@ -34,24 +34,22 @@ RewriteRule ^courses/([^/]+)/scorm/(.*)$ main/document/download_scorm.php?doc_ur
# Rewrite everything in the document folder of a course to the download script
# Except certificate resources, which might need to be accessible publicly to all
RewriteRule ^courses/([^/]+)/document/certificates/(.*)$ app/courses/$1/document/certificates/$2 [QSA,L]
# Note : since version 2.4.38-3 of Apache a security fix had a side effect that made redirection with space not to work.
# Note : since version 2.4.38-3 of Apache a security fix had a side effect that broke redirections with spaces.
# To fix this issue we did not have a common syntaxis but it work with one of those 2 options :
# changing at the end of the following line [QSA,L] for [QSA,L,B=\x20?] or for "[QSA,L,B= ?,BNP]" (with the quotes)
RewriteRule ^courses/([^/]+)/document/(.*)$ main/document/download.php?doc_url=/$2&cDir=$1 [QSA,L]
# We have opted to use the latter by default. If you are encountering "Not found" issues when entering course homepages,
# you might want to try either of the other 2 forms.
RewriteRule ^courses/([^/]+)/document/(.*)$ main/document/download.php?doc_url=/$2&cDir=$1 "[QSA,L,B= ?,BNP]"
# Optimize load of custom per-course icons in courses (avoid download_uploaded_files.php)
RewriteRule ^courses/([^/]+)/upload/course_home_icons/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$ app/courses/$1/upload/course_home_icons/$2 [QSA,L]
# Course upload files
# Note : since version 2.4.38-3 of Apache a security fix had a side effect that made redirection with space not to work.
# To fix this issue we did not have a common syntaxis but it work with one of those 2 options :
# changing at the end of the following line [QSA,L] for [QSA,L,B=\x20?] or for "[QSA,L,B= ?,BNP]" (with the quotes)
RewriteRule ^courses/([^/]+)/upload/([^/]+)/(.*)$ main/document/download_uploaded_files.php?code=$1&type=$2&file=$3 [QSA,L]
# See note on line 37
RewriteRule ^courses/([^/]+)/upload/([^/]+)/(.*)$ main/document/download_uploaded_files.php?code=$1&type=$2&file=$3 "[QSA,L,B= ?,BNP]"
# Rewrite everything in the work folder
# Note : since version 2.4.38-3 of Apache a security fix had a side effect that made redirection with space not to work.
# To fix this issue we did not have a common syntaxis but it work with one of those 2 options :
# changing at the end of the following line [QSA,L] for [QSA,L,B=\x20?] or for "[QSA,L,B= ?,BNP]" (with the quotes)
RewriteRule ^courses/([^/]+)/work/(.*)$ main/work/download.php?file=work/$2&cDir=$1 [QSA,L]
# See note on line 37
RewriteRule ^courses/([^/]+)/work/(.*)$ main/work/download.php?file=work/$2&cDir=$1 "[QSA,L,B= ?,BNP]"
RewriteRule ^courses/([^/]+)/course-pic85x85.png$ main/inc/ajax/course.ajax.php?a=get_course_image&code=$1&image=course_image_source [QSA,L]
RewriteRule ^courses/([^/]+)/course-pic.png$ main/inc/ajax/course.ajax.php?a=get_course_image&code=$1&image=course_image_large_source [QSA,L]
@ -87,10 +85,8 @@ RewriteRule ^service/(\d{1,})$ plugin/buycourses/src/service_information.php?ser
RewriteRule ^lti/os$ plugin/ims_lti/outcome_service.php [L]
# Deny direct access to user my files
# Note : since version 2.4.38-3 of Apache a security fix had a side effect that made redirection with space not to work.
# To fix this issue we did not have a common syntaxis but it work with one of those 2 options :
# changing at the end of the following line [QSA,L] for [QSA,L,B=\x20?] or for "[QSA,L,B= ?,BNP]" (with the quotes)
RewriteRule ^app/upload/users/([^/]+)/([^/]+)/my_files/(.*)$ main/social/download_my_files.php?user_id=$2&file=$3 [QSA,L]
# See note on line 37
RewriteRule ^app/upload/users/([^/]+)/([^/]+)/my_files/(.*)$ main/social/download_my_files.php?user_id=$2&file=$3 "[QSA,L,B= ?,BNP]"
# Deny access
RewriteRule ^(tests|.git) - [F,L,NC]

@ -110,6 +110,50 @@
</table>
<div class="version" aria-label="1.11.30">
<a id="1.11.30"></a>
<h1>Chamilo 1.11.30 - ?, /02/2025</h1>
<h3>Release notes - summary</h3>
<p>Chamilo 1.11.30 is a patch release on top of 1.11.28.</p>
<h3>Release name</h3>
<p><a href="https://en.wikipedia.org/wiki/">?</a> is ...</p>
<h3>Security fixes</h3>
<ul aria-live="off">
</ul>
<h3>Important note</h3>
<p>Chamilo 1.11.30 comes with subtle changes in the root .htaccess file which could affect your system (for example by triggering "Not Found" errors on course homepages) if you use Apache &lt; 2.4.38-3. Please check line 37 of /.htaccess for more info.</p>
<h3>Notable new Features</h3>
<h4>For end-users, teachers and Chamilo admins</h4>
These features are immediately available to users through the web interface.<br />
<ul aria-live="off">
</ul>
<h4>For developers and sysadmins</h4>
Although most features here will be used by teachers or Chamilo admins, they require sysadmin privileges to enable them on the server.
<ul aria-live="off">
</ul>
<h3>Improvements (minor features) and debug</h3>
In reverse chronological order...
<ul aria-live="off">
</ul>
<h3>Stylesheets and theming</h3>
<ul aria-live="off">
<li>No notable style change</li>
</ul>
<h3>Web services</h3>
<ul aria-live="off">
<li>No notable change</li>
</ul>
<h3>Removals</h3>
<ul aria-live="off">
<li>No notable removal</li>
</ul>
<h3>Known issues</h3>
<ul aria-live="off">
<li>No notable known issue</li>
</ul>
</div>
<div class="version" aria-label="1.11.28">
<a id="1.11.28"></a>
<h1>Chamilo 1.11.28 - Alcatraz, 21/10/2024</h1>

@ -171,7 +171,6 @@ function facebookConnect()
/**
* Get facebook login url for the platform.
*
* @return string
* @throws FacebookSDKException
*/
function facebookGetLoginUrl(): string

@ -166,25 +166,25 @@ function external_add_user($u)
* new_user array.
*
* @param array $new_user associative array with the value to upgrade
* WARNING user_id key is MANDATORY
* Possible keys are :
* - firstname
* - lastname
* - username
* - auth_source
* - email
* - status
* - official_code
* - phone
* - picture_uri
* - expiration_date
* - active
* - creator_id
* - hr_dept_id
* - extra : array of custom fields
* - language
* - courses : string of all courses code separated by '|'
* - admin : boolean
* WARNING user_id key is MANDATORY
* Possible keys are :
* - firstname
* - lastname
* - username
* - auth_source
* - email
* - status
* - official_code
* - phone
* - picture_uri
* - expiration_date
* - active
* - creator_id
* - hr_dept_id
* - extra : array of custom fields
* - language
* - courses : string of all courses code separated by '|'
* - admin : boolean
*
* @author ndiechburg <noel@cblue.be>
* */

@ -70,7 +70,7 @@ function generateAikenForm()
if ($hasSingleApi) {
$apiName = $availableApis[$configuredApi] ?? $configuredApi;
$form->addHtml('<div style="margin-bottom: 10px; font-size: 14px; color: #555;">'
. sprintf(get_lang('UsingAIProviderX'), '<strong>'.htmlspecialchars($apiName).'</strong>').'</div>');
.sprintf(get_lang('UsingAIProviderX'), '<strong>'.htmlspecialchars($apiName).'</strong>').'</div>');
}
$form->addElement('text', 'quiz_name', get_lang('QuestionsTopic'));
@ -105,7 +105,7 @@ function generateAikenForm()
var quizName = $("[name=\'quiz_name\']").val();
var nroQ = parseInt($("[name=\'nro_questions\']").val());
var qType = $("[name=\'question_type\']").val();'
. (!$hasSingleApi ? 'var provider = $("[name=\'ai_provider\']").val();' : 'var provider = "' . $configuredApi . '";') .
.(!$hasSingleApi ? 'var provider = $("[name=\'ai_provider\']").val();' : 'var provider = "'.$configuredApi.'";').
'var valid = (quizName != \'\' && nroQ > 0);
if (valid) {
btnGenerate.attr("disabled", true);

@ -208,7 +208,7 @@ class Compilatio
if (isset($dataDocument['analyses'][$anasim]['state'])) {
$documentInfo['analysis_status'] = $dataDocument['analyses'][$anasim]['state'];
}
if (isset($dataDocument['light_reports'][$anasim]['scores']['global_score_percent'])) {
$documentInfo['report_percent'] = $dataDocument['light_reports'][$anasim]['scores']['global_score_percent'];
}

@ -4033,7 +4033,7 @@ function api_not_allowed(
// Check if a custom file (login.tpl) exists for custompages included overrides
if ((!isset($user_id) || api_is_anonymous()) && CustomPages::enabled()) {
$customLoginTemplate = Template::findTemplateFilePath('custompage/login.tpl');
if (file_exists(api_get_path(SYS_TEMPLATE_PATH) . $customLoginTemplate)) {
if (file_exists(api_get_path(SYS_TEMPLATE_PATH).$customLoginTemplate)) {
if (empty($_SESSION['request_uri'])) {
$_SESSION['request_uri'] = $_SERVER['REQUEST_URI'];
}

@ -112,9 +112,6 @@ class HtmlEditor extends HTML_QuickForm_textarea
return $result;
}
/**
* @return string|null
*/
public function getValue(): ?string
{
return RemoveOnAttributes::filter($this->_value);

@ -132,12 +132,11 @@ class GlossaryManager
* This functions stores the glossary in the database.
*
* @param array $values Array of title + description (name => $title, description => $comment)
* @param bool $showMessage
*
* @return bool|int Term id on success, false on failure
*
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*
* @return bool|int Term id on success, false on failure
*/
public static function save_glossary(array $values, bool $showMessage = true)
{
@ -170,7 +169,7 @@ class GlossaryManager
->setDescription($values['description'] ?? "")
->setDisplayOrder($max_glossary_item + 1)
->setSessionId($session_id);
;
Database::getManager()->persist($glossary);
Database::getManager()->flush();

@ -74,7 +74,7 @@ class LpAiHelper
var wordsCount = parseInt($("[name=\'words_count\']").val());
var addTests = $("#add-lp-quiz").is(":checked");
var nroQuestions = parseInt($("[name=\'nro_questions\']").val());
var provider = "' . $configuredApi . '";
var provider = "'.$configuredApi.'";
if (lpName && nroItems > 0 && wordsCount > 0) {
if (addTests && (nroQuestions <= 0 || nroQuestions > 5)) {

@ -4860,12 +4860,8 @@ class learnpath
/**
* Check if the learnpath category is visible for a user.
*
* @param CLpCategory|null $category
* @param User $user
* @param int $courseId
* @param int $sessionId
*
* @return bool
*/
public static function categoryIsVisibleForStudent(
?CLpCategory $category,

@ -370,6 +370,7 @@ if (!empty($userList)) {
$valueA = strtotime($a['registered_at']);
$valueB = strtotime($b['registered_at']);
}
return $sortOrder === SORT_ASC ? $valueA <=> $valueB : $valueB <=> $valueA;
});

@ -61,12 +61,14 @@ var DraggableAnswer = {
DraggableAnswer.trash.droppable({
accept: ".exercise-draggable-answer > li.touch-items",
tolerance: "pointer",
drop: function (e, ui) {
DraggableAnswer.deleteItem(ui.draggable, $(this));
}
});
DraggableAnswer.gallery.droppable({
tolerance: "pointer",
drop: function (e, ui) {
DraggableAnswer.recycleItem(ui.draggable, $(this));
}

@ -3,7 +3,8 @@
use Chamilo\PluginBundle\Entity\AiHelper\Requests;
use Doctrine\ORM\Tools\SchemaTool;
require_once __DIR__ . '/src/deepseek/DeepSeek.php';
require_once __DIR__.'/src/deepseek/DeepSeek.php';
/**
* Description of AiHelperPlugin.
*
@ -112,6 +113,7 @@ class AiHelperPlugin extends Plugin
if (isset($result['error'])) {
$errorMessage = $result['error']['message'] ?? 'Unknown error';
error_log("OpenAI Error: $errorMessage");
return [
'error' => true,
'message' => $errorMessage,
@ -132,11 +134,10 @@ class AiHelperPlugin extends Plugin
}
return $resultText ?: 'No response generated.';
} catch (Exception $e) {
return [
'error' => true,
'message' => 'An error occurred while connecting to OpenAI: ' . $e->getMessage(),
'message' => 'An error occurred while connecting to OpenAI: '.$e->getMessage(),
];
}
}
@ -179,8 +180,9 @@ class AiHelperPlugin extends Plugin
$response = curl_exec($ch);
if ($response === false) {
error_log('cURL error: ' . curl_error($ch));
error_log('cURL error: '.curl_error($ch));
curl_close($ch);
return ['error' => true, 'message' => 'Request to AI provider failed.'];
}
@ -211,12 +213,14 @@ class AiHelperPlugin extends Plugin
/**
* Generate questions based on the selected AI provider.
*
* @param int $nQ Number of questions
* @param string $lang Language for the questions
* @param string $topic Topic of the questions
* @param int $nQ Number of questions
* @param string $lang Language for the questions
* @param string $topic Topic of the questions
* @param string $questionType Type of questions (e.g., 'multiple_choice')
* @return string Questions generated in Aiken format
*
* @throws Exception If an error occurs
*
* @return string Questions generated in Aiken format
*/
public function generateQuestions(int $nQ, string $lang, string $topic, string $questionType = 'multiple_choice'): string
{
@ -232,61 +236,6 @@ class AiHelperPlugin extends Plugin
}
}
/**
* Generate questions using OpenAI.
*/
private function generateOpenAiQuestions(int $nQ, string $lang, string $topic, string $questionType): string
{
$prompt = sprintf(
'Generate %d "%s" questions in Aiken format in the %s language about "%s", making sure there is a \'ANSWER\' line for each question. \'ANSWER\' lines must only mention the letter of the correct answer, not the full answer text and not a parenthesis. The line starting with \'ANSWER\' must not be separated from the last possible answer by a blank line. Each answer starts with an uppercase letter, a dot, one space and the answer text without quotes. Include an \'ANSWER_EXPLANATION\' line after the \'ANSWER\' line for each question. The terms between single quotes above must not be translated. There must be a blank line between each question.',
$nQ,
$questionType,
$lang,
$topic
);
$result = $this->openAiGetCompletionText($prompt, 'quiz');
if (isset($result['error']) && true === $result['error']) {
throw new Exception($result['message']);
}
return $result;
}
/**
* Generate questions using DeepSeek.
*/
private function generateDeepSeekQuestions(int $nQ, string $lang, string $topic, string $questionType): string
{
$apiKey = $this->get('api_key');
$prompt = sprintf(
'Generate %d "%s" questions in Aiken format in the %s language about "%s", making sure there is a \'ANSWER\' line for each question. \'ANSWER\' lines must only mention the letter of the correct answer, not the full answer text and not a parenthesis. The line starting with \'ANSWER\' must not be separated from the last possible answer by a blank line. Each answer starts with an uppercase letter, a dot, one space and the answer text without quotes. Include an \'ANSWER_EXPLANATION\' line after the \'ANSWER\' line for each question. The terms between single quotes above must not be translated. There must be a blank line between each question.',
$nQ,
$questionType,
$lang,
$topic
);
$payload = [
'model' => 'deepseek-chat',
'messages' => [
[
'role' => 'system',
'content' => 'You are a helpful assistant that generates Aiken format questions.',
],
[
'role' => 'user',
'content' => $prompt,
],
],
'stream' => false,
];
$deepSeek = new DeepSeek($apiKey);
$response = $deepSeek->generateQuestions($payload);
return $response;
}
/**
* Validates tokens limit of a user per current month.
*/
@ -414,4 +363,59 @@ class AiHelperPlugin extends Plugin
]
);
}
/**
* Generate questions using OpenAI.
*/
private function generateOpenAiQuestions(int $nQ, string $lang, string $topic, string $questionType): string
{
$prompt = sprintf(
'Generate %d "%s" questions in Aiken format in the %s language about "%s", making sure there is a \'ANSWER\' line for each question. \'ANSWER\' lines must only mention the letter of the correct answer, not the full answer text and not a parenthesis. The line starting with \'ANSWER\' must not be separated from the last possible answer by a blank line. Each answer starts with an uppercase letter, a dot, one space and the answer text without quotes. Include an \'ANSWER_EXPLANATION\' line after the \'ANSWER\' line for each question. The terms between single quotes above must not be translated. There must be a blank line between each question.',
$nQ,
$questionType,
$lang,
$topic
);
$result = $this->openAiGetCompletionText($prompt, 'quiz');
if (isset($result['error']) && true === $result['error']) {
throw new Exception($result['message']);
}
return $result;
}
/**
* Generate questions using DeepSeek.
*/
private function generateDeepSeekQuestions(int $nQ, string $lang, string $topic, string $questionType): string
{
$apiKey = $this->get('api_key');
$prompt = sprintf(
'Generate %d "%s" questions in Aiken format in the %s language about "%s", making sure there is a \'ANSWER\' line for each question. \'ANSWER\' lines must only mention the letter of the correct answer, not the full answer text and not a parenthesis. The line starting with \'ANSWER\' must not be separated from the last possible answer by a blank line. Each answer starts with an uppercase letter, a dot, one space and the answer text without quotes. Include an \'ANSWER_EXPLANATION\' line after the \'ANSWER\' line for each question. The terms between single quotes above must not be translated. There must be a blank line between each question.',
$nQ,
$questionType,
$lang,
$topic
);
$payload = [
'model' => 'deepseek-chat',
'messages' => [
[
'role' => 'system',
'content' => 'You are a helpful assistant that generates Aiken format questions.',
],
[
'role' => 'user',
'content' => $prompt,
],
],
'stream' => false,
];
$deepSeek = new DeepSeek($apiKey);
$response = $deepSeek->generateQuestions($payload);
return $response;
}
}

@ -21,8 +21,10 @@ class DeepSeek
* Generate questions using the DeepSeek API.
*
* @param array $payload Data to send to the API
* @return string Decoded response from the API
*
* @throws Exception If an error occurs during the request
*
* @return string Decoded response from the API
*/
public function generateQuestions(array $payload): string
{
@ -51,11 +53,13 @@ class DeepSeek
/**
* Send a request to the DeepSeek API.
*
* @param string $url Endpoint to send the request to
* @param string $url Endpoint to send the request to
* @param string $method HTTP method (e.g., GET, POST)
* @param array $data Data to send as JSON
* @return string Raw response from the API
* @param array $data Data to send as JSON
*
* @throws Exception If a cURL error occurs
*
* @return string Raw response from the API
*/
private function sendRequest(string $url, string $method, array $data = []): string
{

@ -12,6 +12,6 @@ class DeepSeekUrl
*/
public static function completionsUrl(): string
{
return self::BASE_URL . '/completions';
return self::BASE_URL.'/completions';
}
}

@ -22,10 +22,9 @@ try {
'text' => trim($resultText),
]);
} catch (Exception $e) {
error_log("Error: " . $e->getMessage());
error_log("Error: ".$e->getMessage());
echo json_encode([
'success' => false,
'text' => $e->getMessage(),
]);
}

@ -22,7 +22,6 @@ if (!in_array($apiName, array_keys($apiList))) {
exit;
}
$courseLanguage = (string) $_REQUEST['language'];
$chaptersCount = (int) $_REQUEST['nro_items'];
$topic = (string) $_REQUEST['lp_name'];

@ -384,6 +384,29 @@ class AzureActiveDirectory extends Plugin
];
}
public function getSyncState(string $title): ?AzureSyncState
{
$stateRepo = Database::getManager()->getRepository(AzureSyncState::class);
return $stateRepo->findOneBy(['title' => $title]);
}
public function saveSyncState(string $title, $value)
{
$state = $this->getSyncState($title);
if (!$state) {
$state = new AzureSyncState();
$state->setTitle($title);
Database::getManager()->persist($state);
}
$state->setValue($value);
Database::getManager()->flush();
}
/**
* @throws Exception
*/
@ -425,27 +448,4 @@ class AzureActiveDirectory extends Plugin
$extra,
];
}
public function getSyncState(string $title): ?AzureSyncState
{
$stateRepo = Database::getManager()->getRepository(AzureSyncState::class);
return $stateRepo->findOneBy(['title' => $title]);
}
public function saveSyncState(string $title, $value)
{
$state = $this->getSyncState($title);
if (!$state) {
$state = new AzureSyncState();
$state->setTitle($title);
Database::getManager()->persist($state);
}
$state->setValue($value);
Database::getManager()->flush();
}
}

@ -21,8 +21,6 @@ class AzureSyncState
public const USERGROUPS_DATALINK = 'usergroups_datalink';
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id()
* @ORM\GeneratedValue()
@ -30,15 +28,11 @@ class AzureSyncState
private int $id = 0;
/**
* @var string
*
* @ORM\Column(name="title", type="string")
*/
private string $title;
/**
* @var string
*
* @ORM\Column(name="value", type="text")
*/
private string $value;

Loading…
Cancel
Save