diff --git a/main/cron/import_csv.php b/main/cron/import_csv.php
index d4b4e0625c..2c2be5a24b 100755
--- a/main/cron/import_csv.php
+++ b/main/cron/import_csv.php
@@ -798,6 +798,15 @@ class ImportCsv
0 //$reset_password = 0
);
+ $table = Database::get_main_table(TABLE_MAIN_USER);
+ $authSource = Database::escape_string($row['auth_source']);
+ $sql = "UPDATE $table SET auth_source = '$authSource' WHERE id = ".$userInfo['user_id'];
+ Database::query($sql);
+
+ $this->logger->addInfo(
+ 'Teachers - #'.$userInfo['user_id']." auth_source was changed from '".$userInfo['auth_source']."' to '".$row['auth_source']."' "
+ );
+
if ($result) {
foreach ($row as $key => $value) {
if (substr($key, 0, 6) == 'extra_') {
@@ -1064,6 +1073,15 @@ class ImportCsv
$resetPassword //$reset_password = 0
);
+ $table = Database::get_main_table(TABLE_MAIN_USER);
+ $authSource = Database::escape_string($row['auth_source']);
+ $sql = "UPDATE $table SET auth_source = '$authSource' WHERE id = ".$userInfo['user_id'];
+ Database::query($sql);
+
+ $this->logger->addInfo(
+ "Students - #".$userInfo['user_id']." auth_source was changed from '".$userInfo['auth_source']."' to '".$row['auth_source']."' "
+ );
+
if ($result) {
if ($row['username'] != $userInfo['username']) {
$this->logger->addInfo("Students - Username was changes from '".$userInfo['username']."' to '".$row['username']."' ");
diff --git a/main/exercise/comparative_group_report.php b/main/exercise/comparative_group_report.php
index 8068fe8918..6a53aa034c 100644
--- a/main/exercise/comparative_group_report.php
+++ b/main/exercise/comparative_group_report.php
@@ -14,7 +14,7 @@ if (!$isAllowedToEdit) {
}
$exerciseId = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : 0;
-
+$exportXls = isset($_GET['export_xls']) && !empty($_GET['export_xls']) ? (int) $_GET['export_xls'] : 0;
if (empty($exerciseId)) {
api_not_allowed(true);
}
@@ -59,6 +59,9 @@ foreach ($headers as $header) {
$table->setHeaderContents($row, $column, $header);
$column++;
}
+if ($exportXls) {
+ $tableXls[] = $headers;
+}
$row++;
$scoreDisplay = new ScoreDisplay();
@@ -69,10 +72,32 @@ if (!empty($groups)) {
$table->setCellContents($row, 0, $group['name']);
$averageToDisplay = $scoreDisplay->display_score([$average, 1], SCORE_AVERAGE);
$table->setCellContents($row, 1, $averageToDisplay);
+ if ($exportXls) {
+ $tableXls[] = [$group['name'], $averageToDisplay];
+ }
$row++;
}
}
-
+if ($exportXls) {
+ $fileName = get_lang('ComparativeGroupReport').'_'.api_get_course_id().'_'.$exerciseId.'_'.api_get_local_time();
+ Export::arrayToXls($tableXls, $fileName);
+ exit;
+}
Display::display_header($nameTools, get_lang('Exercise'));
+$actions = ''.
+ Display:: return_icon(
+ 'back.png',
+ get_lang('GoBackToQuestionList'),
+ '',
+ ICON_SIZE_MEDIUM
+ )
+ .'';
+$actions .= Display::url(
+ Display::return_icon('excel.png', get_lang('ExportAsXLS'), [], ICON_SIZE_MEDIUM),
+ 'comparative_group_report.php?id='.$exerciseId.'&export_xls=1&'.api_get_cidreq()
+);
+
+$actions = Display::div($actions, ['class' => 'actions']);
+echo $actions;
echo $table->toHtml();
Display::display_footer();
diff --git a/main/exercise/question_stats.php b/main/exercise/question_stats.php
index 0613da1079..733c9018a6 100644
--- a/main/exercise/question_stats.php
+++ b/main/exercise/question_stats.php
@@ -14,6 +14,7 @@ if (!$isAllowedToEdit) {
}
$exerciseId = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : 0;
+$exportXls = isset($_REQUEST['export_xls']) && !empty($_REQUEST['export_xls']) ? (int) $_REQUEST['export_xls'] : 0;
$groups = $_REQUEST['groups'] ?? [];
$users = $_REQUEST['users'] ?? [];
@@ -84,7 +85,7 @@ $form->addSelect(
]
);
-$form->addButtonSearch(get_lang('Search'));
+$form->addButtonSearch(get_lang('Search'), 'searchSubmit');
$formToString = $form->toHtml();
@@ -101,10 +102,12 @@ foreach ($headers as $header) {
$table->setHeaderContents($row, $column, $header);
$column++;
}
+if ($exportXls) {
+ $tableXls[] = $headers;
+}
$row++;
$scoreDisplay = new ScoreDisplay();
$orderedData = [];
-
if ($form->validate()) {
$questions = ExerciseLib::getWrongQuestionResults($courseId, $exerciseId, $sessionId, $groups, $users);
foreach ($questions as $data) {
@@ -117,22 +120,27 @@ if ($form->validate()) {
$groups,
$users
);
- $orderedData[] = [
+ $ordered = [
$data['question'],
$data['count'].' / '.$total,
$scoreDisplay->display_score([$data['count'], $total], SCORE_AVERAGE),
];
+ $orderedData[] = $ordered;
+ if ($exportXls) {
+ $tableXls[] = $ordered;
+ }
}
} else {
$questions = ExerciseLib::getWrongQuestionResults($courseId, $exerciseId, $sessionId);
foreach ($questions as $data) {
$questionId = (int) $data['question_id'];
$total = ExerciseLib::getTotalQuestionAnswered($courseId, $exerciseId, $questionId, $sessionId);
- $orderedData[] = [
+ $ordered = [
$data['question'],
$data['count'].' / '.$total,
$scoreDisplay->display_score([$data['count'], $total], SCORE_AVERAGE),
];
+ $orderedData[] = $ordered;
}
}
@@ -151,8 +159,51 @@ foreach ($headers as $header) {
$table->set_header($column, $header, false);
$column++;
}
+if ($exportXls) {
+ $fileName = get_lang('QuestionStats').'_'.api_get_course_id().'_'.$exerciseId.'_'.api_get_local_time();
+ Export::arrayToXls($tableXls, $fileName);
+ exit;
+}
+$htmlHeadXtra[] = '';
Display::display_header($nameTools, get_lang('Exercise'));
+$actions = ''.
+ Display:: return_icon(
+ 'back.png',
+ get_lang('GoBackToQuestionList'),
+ '',
+ ICON_SIZE_MEDIUM
+ )
+ .'';
+$actions .= Display::url(
+ Display::return_icon('excel.png', get_lang('ExportAsXLS'), [], ICON_SIZE_MEDIUM),
+ '#',
+ ['id' => 'export-xls']
+);
+
+$actions = Display::div($actions, ['class' => 'actions']);
+
+echo $actions;
echo $formToString;
echo $table->return_table();
Display::display_footer();
diff --git a/main/exercise/stats.php b/main/exercise/stats.php
index 02218dadc1..f6645d8d64 100755
--- a/main/exercise/stats.php
+++ b/main/exercise/stats.php
@@ -20,6 +20,7 @@ if (!$showPage) {
api_not_allowed(true);
}
+$exportXls = isset($_GET['export_xls']) && !empty($_GET['export_xls']) ? (int) $_GET['export_xls'] : 0;
$exerciseId = isset($_GET['exerciseId']) && !empty($_GET['exerciseId']) ? (int) $_GET['exerciseId'] : 0;
$objExercise = new Exercise();
$result = $objExercise->read($exerciseId);
@@ -77,12 +78,15 @@ if (!empty($questionList)) {
if ($count_students) {
$percentage = $count_users / $count_students * 100;
}
-
- $data[$question_id]['students_who_try_exercise'] = Display::bar_progress(
- $percentage,
- false,
- $count_users.' / '.$count_students
- );
+ if ($exportXls) {
+ $data[$question_id]['students_who_try_exercise'] = $count_users.' / '.$count_students.' ('.$percentage.'%)';
+ } else {
+ $data[$question_id]['students_who_try_exercise'] = Display::bar_progress(
+ $percentage,
+ false,
+ $count_users.' / '.$count_students
+ );
+ }
$data[$question_id]['lowest_score'] = round($exerciseStats['min'], 2);
$data[$question_id]['average_score'] = round($exerciseStats['average'], 2);
$data[$question_id]['highest_score'] = round($exerciseStats['max'], 2);
@@ -97,15 +101,24 @@ foreach ($headers as $header) {
$table->setHeaderContents($row, $column, $header);
$column++;
}
+if ($exportXls) {
+ $tableXls1[] = $headers;
+}
$row++;
foreach ($data as $row_table) {
$column = 0;
- foreach ($row_table as $cell) {
+ foreach ($row_table as $key => $cell) {
$table->setCellContents($row, $column, $cell);
$table->updateCellAttributes($row, $column, 'align="center"');
+ if ($exportXls) {
+ $row_table[$key] = strip_tags($cell);
+ }
$column++;
}
$table->updateRowAttributes($row, $row % 2 ? 'class="row_even"' : 'class="row_odd"', true);
+ if ($exportXls) {
+ $tableXls1[] = $row_table;
+ }
$row++;
}
$content = $table->toHtml();
@@ -170,11 +183,15 @@ if (!empty($questionList)) {
if (!empty($count_students)) {
$percentage = $count / $count_students * 100;
}
- $data[$id]['attempts'] = Display::bar_progress(
- $percentage,
- false,
- $count.' / '.$count_students
- );
+ if ($exportXls) {
+ $data[$id]['attempts'] = $count.' / '.$count_students.' ('.$percentage.'%)';
+ } else {
+ $data[$id]['attempts'] = Display::bar_progress(
+ $percentage,
+ false,
+ $count.' / '.$count_students
+ );
+ }
$id++;
$counter++;
}
@@ -210,11 +227,15 @@ if (!empty($questionList)) {
if (!empty($count_students)) {
$percentage = $count / $count_students * 100;
}
- $data[$id]['attempts'] = Display::bar_progress(
- $percentage,
- false,
- $count.' / '.$count_students
- );
+ if ($exportXls) {
+ $data[$id]['attempts'] = $count.' / '.$count_students.' ('.$percentage.'%)';
+ } else {
+ $data[$id]['attempts'] = Display::bar_progress(
+ $percentage,
+ false,
+ $count.' / '.$count_students
+ );
+ }
}
break;
case HOT_SPOT:
@@ -237,11 +258,15 @@ if (!empty($questionList)) {
if (!empty($count_students)) {
$percentage = $count / $count_students * 100;
}
- $data[$id]['attempts'] = Display::bar_progress(
- $percentage,
- false,
- $count.' / '.$count_students
- );
+ if ($exportXls) {
+ $data[$id]['attempts'] = $count.' / '.$count_students.' ('.$percentage.'%)';
+ } else {
+ $data[$id]['attempts'] = Display::bar_progress(
+ $percentage,
+ false,
+ $count.' / '.$count_students
+ );
+ }
break;
default:
if ($answer_id == 1) {
@@ -263,11 +288,15 @@ if (!empty($questionList)) {
if (!empty($count_students)) {
$percentage = $count / $count_students * 100;
}
- $data[$id]['attempts'] = Display::bar_progress(
- $percentage,
- false,
- $count.' / '.$count_students
- );
+ if ($exportXls) {
+ $data[$id]['attempts'] = $count.' / '.$count_students.' ('.$percentage.'%)';
+ } else {
+ $data[$id]['attempts'] = Display::bar_progress(
+ $percentage,
+ false,
+ $count.' / '.$count_students
+ );
+ }
}
$id++;
}
@@ -282,15 +311,25 @@ foreach ($headers as $header) {
$table->setHeaderContents($row, $column, $header);
$column++;
}
+if ($exportXls) {
+ $tableXls1[] = []; // it adds an empty line after the first table
+ $tableXls2[] = $headers;
+}
$row++;
foreach ($data as $row_table) {
$column = 0;
- foreach ($row_table as $cell) {
+ foreach ($row_table as $key => $cell) {
$table->setCellContents($row, $column, $cell);
$table->updateCellAttributes($row, $column, 'align="center"');
+ if ($exportXls) {
+ $row_table[$key] = strip_tags($cell);
+ }
$column++;
}
$table->updateRowAttributes($row, $row % 2 ? 'class="row_even"' : 'class="row_odd"', true);
+ if ($exportXls) {
+ $tableXls2[] = $row_table;
+ }
$row++;
}
$content .= $table->toHtml();
@@ -309,6 +348,12 @@ if ($exportPdf) {
Export::export_html_to_pdf($content, $params);
exit;
}
+if ($exportXls) {
+ $fileName = get_lang('Report').'_'.api_get_course_id().'_'.api_get_local_time();
+ $tableXls = array_merge($tableXls1, $tableXls2);
+ Export::arrayToXls($tableXls, $fileName);
+ exit;
+}
$interbreadcrumb[] = [
"url" => "exercise.php?".api_get_cidreq(),
@@ -332,6 +377,11 @@ $actions .= Display::url(
Display::return_icon('pdf.png', get_lang('ExportToPDF'), [], ICON_SIZE_MEDIUM),
'stats.php?exerciseId='.$exerciseId.'&export_pdf=1&'.api_get_cidreq()
);
+$actions .= Display::url(
+ Display::return_icon('excel.png', get_lang('ExportAsXLS'), [], ICON_SIZE_MEDIUM),
+ 'stats.php?exerciseId='.$exerciseId.'&export_xls=1&'.api_get_cidreq()
+);
+
$actions = Display::div($actions, ['class' => 'actions']);
$content = $actions.$content;
$tpl->assign('content', $content);
diff --git a/main/inc/ajax/exercise.ajax.php b/main/inc/ajax/exercise.ajax.php
index 8c4904473e..9ef58bbeaf 100755
--- a/main/inc/ajax/exercise.ajax.php
+++ b/main/inc/ajax/exercise.ajax.php
@@ -670,6 +670,14 @@ switch ($action) {
if ($objQuestionTmp->type === UPLOAD_ANSWER) {
$my_choice = '';
if (!empty($uploadAnswerFileNames)) {
+ // Clean user upload_answer folder
+ $userUploadAnswerSyspath = UserManager::getUserPathById(api_get_user_id(), 'system').'my_files'.'/upload_answer/'.$exeId.'/'.$my_question_id.'/*';
+ foreach (glob($userUploadAnswerSyspath) as $file) {
+ $filename = basename($file);
+ if (!in_array($filename, $uploadAnswerFileNames[$my_question_id])) {
+ unlink($file);
+ }
+ }
$my_choice = implode('|', $uploadAnswerFileNames[$my_question_id]);
}
}
diff --git a/main/inc/lib/agenda.lib.php b/main/inc/lib/agenda.lib.php
index 58d5f185b8..65520eb35f 100644
--- a/main/inc/lib/agenda.lib.php
+++ b/main/inc/lib/agenda.lib.php
@@ -475,7 +475,7 @@ class Agenda
*
* @throws Exception
*
- * @return array
+ * @return array with local times
*/
public function generateDatesByType($type, $startEvent, $endEvent, $repeatUntilDate)
{
@@ -528,27 +528,33 @@ class Agenda
break;
}
- // @todo remove comment code
- $startDateInLocal = new DateTime($newStartDate, new DateTimeZone($timeZone));
+ // @todo remove comment code
+ // The code below was not adpating to saving light time but was doubling the difference with UTC time.
+ // Might be necessary to adapt to update saving light time difference.
+/* $startDateInLocal = new DateTime($newStartDate, new DateTimeZone($timeZone));
if ($startDateInLocal->format('I') == 0) {
// Is saving time? Then fix UTC time to add time
$seconds = $startDateInLocal->getOffset();
$startDate->add(new DateInterval("PT".$seconds."S"));
- $startDateFixed = $startDate->format('Y-m-d H:i:s');
- $startDateInLocalFixed = new DateTime($startDateFixed, new DateTimeZone($timeZone));
- $newStartDate = $startDateInLocalFixed->format('Y-m-d H:i:s');
+ //$startDateFixed = $startDate->format('Y-m-d H:i:s');
+ //$startDateInLocalFixed = new DateTime($startDateFixed, new DateTimeZone($timeZone));
+ //$newStartDate = $startDateInLocalFixed->format('Y-m-d H:i:s');
+ //$newStartDate = $startDate->setTimezone(new DateTimeZone($timeZone))->format('Y-m-d H:i:s');
}
- $endDateInLocal = new DateTime($newEndDate, new DateTimeZone($timeZone));
+ $endDateInLocal = new DateTime($newEndDate, new DateTimeZone($timeZone));
if ($endDateInLocal->format('I') == 0) {
// Is saving time? Then fix UTC time to add time
$seconds = $endDateInLocal->getOffset();
$endDate->add(new DateInterval("PT".$seconds."S"));
- $endDateFixed = $endDate->format('Y-m-d H:i:s');
- $endDateInLocalFixed = new DateTime($endDateFixed, new DateTimeZone($timeZone));
- $newEndDate = $endDateInLocalFixed->format('Y-m-d H:i:s');
- }
- $list[] = ['start' => $newStartDate, 'end' => $newEndDate, 'i' => $startDateInLocal->format('I')];
+ //$endDateFixed = $endDate->format('Y-m-d H:i:s');
+ //$endDateInLocalFixed = new DateTime($endDateFixed, new DateTimeZone($timeZone));
+ //$newEndDate = $endDateInLocalFixed->format('Y-m-d H:i:s');
+ }
+*/
+ $newStartDate = $startDate->setTimezone(new DateTimeZone($timeZone))->format('Y-m-d H:i:s');
+ $newEndDate = $endDate->setTimezone(new DateTimeZone($timeZone))->format('Y-m-d H:i:s');
+ $list[] = ['start' => $newStartDate, 'end' => $newEndDate];
$counter++;
// just in case stop if more than $loopMax
@@ -605,8 +611,10 @@ class Agenda
$now = time();
// The event has to repeat *in the future*. We don't allow repeated
- // events in the past
- if ($end > $now) {
+ // events in the past.
+ $endTimeStamp = api_strtotime($end, 'UTC');
+
+ if ($endTimeStamp < $now) {
return false;
}
@@ -618,7 +626,7 @@ class Agenda
$type = Database::escape_string($type);
$end = Database::escape_string($end);
- $endTimeStamp = api_strtotime($end, 'UTC');
+
$sql = "INSERT INTO $t_agenda_r (c_id, cal_id, cal_type, cal_end)
VALUES ($courseId, '$eventId', '$type', '$endTimeStamp')";
Database::query($sql);
@@ -636,6 +644,7 @@ class Agenda
// just before the part updating the date in local time so keep both synchronised
$start = $dateInfo['start'];
$end = $dateInfo['end'];
+
$this->addEvent(
$start,
$end,
diff --git a/main/inc/lib/exercise.lib.php b/main/inc/lib/exercise.lib.php
index ac21447ade..a90e79d211 100644
--- a/main/inc/lib/exercise.lib.php
+++ b/main/inc/lib/exercise.lib.php
@@ -215,8 +215,20 @@ class ExerciseLib
'#',
['enctype' => 'multipart/form-data']
);
+ $iconDelete = Display::return_icon('delete.png', get_lang('Delete'), [], ICON_SIZE_SMALL);
$multipleForm->addMultipleUpload($url);
$s .= '';
diff --git a/main/inc/lib/online.inc.php b/main/inc/lib/online.inc.php
index 28c94f1890..d4007237b9 100755
--- a/main/inc/lib/online.inc.php
+++ b/main/inc/lib/online.inc.php
@@ -83,7 +83,7 @@ function preventMultipleLogin($userId)
$currentIp = Session::read('current_ip');
$differentIp = false;
if (!empty($currentIp) && api_get_real_ip() !== $currentIp) {
- $isFirstLogin = null;
+ //$isFirstLogin = null;
$differentIp = true;
}
diff --git a/main/install/configuration.dist.php b/main/install/configuration.dist.php
index e5bc399520..7b98d8c290 100755
--- a/main/install/configuration.dist.php
+++ b/main/install/configuration.dist.php
@@ -263,6 +263,8 @@ $_configuration['system_stable'] = NEW_VERSION_STABLE;
//$_configuration['lp_replace_http_to_https'] = false;
// Fix embedded videos inside lps, adding an optional popup
//$_configuration['lp_fix_embed_content'] = false;
+// Check the prerequisite in lp of a quiz to use only the last score in the attempts
+// $_configuration['lp_prerequisite_use_last_attempt_only'] = false;
// Manage deleted files marked with "DELETED" (by course and only by allowed by admin)
//$_configuration['document_manage_deleted_files'] = false;
// Hide tabs in the main/session/index.php page
@@ -1987,6 +1989,9 @@ VALUES (21, 13, 'send_notification_at_a_specific_date', 'Send notification at a
// Enable image upload as file when doing a copy in the content or a drag and drop.
//$_configuration['enable_uploadimage_editor'] = false;
+// Ckeditor settings.
+//$_configuration['editor_settings'] = ['config' => ['youtube_responsive' => true]];
+
// KEEP THIS AT THE END
// -------- Custom DB changes
// Add user activation by confirmation email
diff --git a/main/lp/learnpathItem.class.php b/main/lp/learnpathItem.class.php
index 4905247fe1..a1997c97a7 100755
--- a/main/lp/learnpathItem.class.php
+++ b/main/lp/learnpathItem.class.php
@@ -2354,6 +2354,8 @@ class learnpathItem
if ($this->prevent_reinit == 1) {
// 2. If is completed we check the results in the DB of the quiz.
if ($returnstatus) {
+ $checkLastScoreAttempt = api_get_configuration_value('lp_prerequisite_use_last_attempt_only');
+ $orderBy = ($checkLastScoreAttempt ? 'ORDER BY exe_date DESC' : 'ORDER BY (exe_result/exe_weighting) DESC');
$sql = 'SELECT exe_result, exe_weighting
FROM '.Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES).'
WHERE
@@ -2363,7 +2365,7 @@ class learnpathItem
orig_lp_item_id = '.$prereqs_string.' AND
status <> "incomplete" AND
c_id = '.$courseId.'
- ORDER BY exe_date DESC
+ '.$orderBy.'
LIMIT 0, 1';
$rs_quiz = Database::query($sql);
if ($quiz = Database::fetch_array($rs_quiz)) {
diff --git a/main/mySpace/student_follow_export.php b/main/mySpace/student_follow_export.php
index aa78600042..89e411a877 100644
--- a/main/mySpace/student_follow_export.php
+++ b/main/mySpace/student_follow_export.php
@@ -74,7 +74,8 @@ function generateForm(int $studentId, array $coursesInSessions): FormValidator
[],
FormValidator::LAYOUT_BOX
);
-
+ // Option to hide the column Time in lp tables
+ $form->addCheckBox('hide_connection_time', null, get_lang('HideConnectionTime'));
foreach ($coursesInSessions as $sId => $courses) {
if (empty($courses)) {
continue;
@@ -126,15 +127,17 @@ function generateForm(int $studentId, array $coursesInSessions): FormValidator
return $form;
}
-function generateHtmlForLearningPaths(int $studentId, array $courseInfo, int $sessionId): string
+function generateHtmlForLearningPaths(int $studentId, array $courseInfo, int $sessionId, bool $hideConnectionTime = false): string
{
$student = api_get_user_entity($studentId);
-
+ $showTime = ($hideConnectionTime === false);
$html = '';
$columnHeaders = [];
$columnHeaders['lp'] = get_lang('LearningPath');
- $columnHeaders['time'] = get_lang('Time');
+ if ($showTime) {
+ $columnHeaders['time'] = get_lang('Time');
+ }
$columnHeaders['best_score'] = get_lang('BestScore');
$columnHeaders['latest_attempt_avg_score'] = get_lang('LatestAttemptAverageScore');
$columnHeaders['progress'] = get_lang('Progress');
@@ -208,7 +211,7 @@ function generateHtmlForLearningPaths(int $studentId, array $courseInfo, int $se
$contentToExport[] = api_html_entity_decode(stripslashes($learnpath['lp_name']), ENT_QUOTES);
}
- if (in_array('time', $columnHeadersKeys)) {
+ if (in_array('time', $columnHeadersKeys) && $showTime) {
// Get time in lp
if (!empty($timeCourse)) {
$lpTime = $timeCourse[TOOL_LEARNPATH] ?? 0;
@@ -456,7 +459,7 @@ function generateHtmlForTasks(int $studentId, array $courseInfo, int $sessionId)
.Export::convert_array_to_html($taskTable);
}
-function generateHtmlForCourse(int $studentId, array $coursesInSessions, int $courseId, int $sessionId): ?string
+function generateHtmlForCourse(int $studentId, array $coursesInSessions, int $courseId, int $sessionId, bool $hideConnectionTime = false): ?string
{
if (empty($coursesInSessions[$sessionId]) || !in_array($courseId, $coursesInSessions[$sessionId])) {
return null;
@@ -479,7 +482,7 @@ function generateHtmlForCourse(int $studentId, array $coursesInSessions, int $co
$courseHtml[] = Display::page_header($courseInfo['title']);
}
- $courseHtml[] = generateHtmlForLearningPaths($studentId, $courseInfo, $sessionId);
+ $courseHtml[] = generateHtmlForLearningPaths($studentId, $courseInfo, $sessionId, $hideConnectionTime);
$courseHtml[] = generateHtmlForQuizzes($studentId, $courseInfo, $sessionId);
$courseHtml[] = generateHtmlForTasks($studentId, $courseInfo, $sessionId);
@@ -506,12 +509,12 @@ if ($form->validate()) {
);
$coursesInfo = [];
-
+ $hideConnectionTime = isset($values['hide_connection_time']);
if (!empty($values['sc'])) {
foreach ($values['sc'] as $courseKey) {
[$sessionId, $courseId] = explode('_', $courseKey);
- $coursesInfo[] = generateHtmlForCourse($studentInfo['id'], $coursesInSessions, $courseId, $sessionId);
+ $coursesInfo[] = generateHtmlForCourse($studentInfo['id'], $coursesInSessions, $courseId, $sessionId, $hideConnectionTime);
}
}
diff --git a/plugin/exercise_signature/lib/ExerciseSignature.php b/plugin/exercise_signature/lib/ExerciseSignature.php
index ad4e198aa3..acd896bc00 100644
--- a/plugin/exercise_signature/lib/ExerciseSignature.php
+++ b/plugin/exercise_signature/lib/ExerciseSignature.php
@@ -28,13 +28,13 @@ class ExerciseSignaturePlugin extends Plugin
public static function exerciseHasSignatureActivated(Exercise $exercise)
{
- if (empty($exercise->iId)) {
+ if (empty($exercise->iid)) {
return false;
}
if ('true' === api_get_plugin_setting('exercise_signature', 'tool_enable')) {
$extraFieldValue = new ExtraFieldValue('exercise');
- $result = $extraFieldValue->get_values_by_handler_and_field_variable($exercise->iId, 'signature_activated');
+ $result = $extraFieldValue->get_values_by_handler_and_field_variable($exercise->iid, 'signature_activated');
if ($result && isset($result['value']) && 1 === (int) $result['value']) {
return true;
}
diff --git a/plugin/studentfollowup/StudentFollowUpPlugin.php b/plugin/studentfollowup/StudentFollowUpPlugin.php
index b76e1ba10f..6e9733f6df 100644
--- a/plugin/studentfollowup/StudentFollowUpPlugin.php
+++ b/plugin/studentfollowup/StudentFollowUpPlugin.php
@@ -126,7 +126,7 @@ class StudentFollowUpPlugin extends Plugin
}
// Student sessions.
- $sessions = SessionManager::get_sessions_by_user($studentId, false, true);
+ $sessions = SessionManager::get_sessions_by_user($studentId, true, true);
if (!empty($sessions)) {
foreach ($sessions as $session) {
$sessionId = $session['session_id'];
diff --git a/plugin/xapi/.htaccess b/plugin/xapi/.htaccess
new file mode 100644
index 0000000000..a3851a941e
--- /dev/null
+++ b/plugin/xapi/.htaccess
@@ -0,0 +1 @@
+AcceptPathInfo On
\ No newline at end of file
diff --git a/plugin/xapi/README.md b/plugin/xapi/README.md
index a59cc1a7c7..f9ba489055 100644
--- a/plugin/xapi/README.md
+++ b/plugin/xapi/README.md
@@ -66,4 +66,5 @@ ALTER TABLE xapi_cmi5_item ADD CONSTRAINT FK_7CA116D8A977936C FOREIGN KEY (tree_
ALTER TABLE xapi_cmi5_item ADD CONSTRAINT FK_7CA116D8727ACA70 FOREIGN KEY (parent_id) REFERENCES xapi_cmi5_item (id) ON DELETE CASCADE;
CREATE TABLE xapi_activity_state (id INT AUTO_INCREMENT NOT NULL, state_id VARCHAR(255) NOT NULL, activity_id VARCHAR(255) NOT NULL, agent LONGTEXT NOT NULL COMMENT '(DC2Type:json)', document_data LONGTEXT NOT NULL COMMENT '(DC2Type:json)', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
+CREATE TABLE xapi_activity_profile (id INT AUTO_INCREMENT NOT NULL, profile_id VARCHAR(255) NOT NULL, activity_id VARCHAR(255) NOT NULL, document_data LONGTEXT NOT NULL COMMENT '(DC2Type:json)', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
```
\ No newline at end of file
diff --git a/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementGetController.php b/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementGetController.php
index 17c3cc9bf8..88469b3371 100644
--- a/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementGetController.php
+++ b/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementGetController.php
@@ -17,6 +17,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Xabbuh\XApi\Common\Exception\NotFoundException;
+use Xabbuh\XApi\Model\IRL;
use Xabbuh\XApi\Model\Statement;
use Xabbuh\XApi\Model\StatementId;
use Xabbuh\XApi\Model\StatementResult;
@@ -30,9 +31,9 @@ use XApi\Repository\Api\StatementRepositoryInterface;
/**
* @author Jérôme Parmentier
*/
-final class StatementGetController
+class StatementGetController
{
- private static $getParameters = [
+ protected static $getParameters = [
'statementId' => true,
'voidedStatementId' => true,
'agent' => true,
@@ -47,12 +48,13 @@ final class StatementGetController
'format' => true,
'attachments' => true,
'ascending' => true,
+ 'cursor' => true,
];
- private $repository;
- private $statementSerializer;
- private $statementResultSerializer;
- private $statementsFilterFactory;
+ protected $repository;
+ protected $statementSerializer;
+ protected $statementResultSerializer;
+ protected $statementsFilterFactory;
public function __construct(StatementRepositoryInterface $repository, StatementSerializerInterface $statementSerializer, StatementResultSerializerInterface $statementResultSerializer, StatementsFilterFactory $statementsFilterFactory)
{
@@ -86,14 +88,19 @@ final class StatementGetController
} else {
$statements = $this->repository->findStatementsBy($this->statementsFilterFactory->createFromParameterBag($query));
- $response = $this->buildMultiStatementsResponse($statements, $includeAttachments);
+ $response = $this->buildMultiStatementsResponse($statements, $query, $includeAttachments);
}
} catch (NotFoundException $e) {
- $response = $this->buildMultiStatementsResponse([]);
+ $response = $this->buildMultiStatementsResponse([], $query)
+ ->setStatusCode(Response::HTTP_NOT_FOUND)
+ ->setContent('');
+ } catch (\Exception $exception) {
+ $response = Response::create('', Response::HTTP_BAD_REQUEST);
}
$now = new \DateTime();
$response->headers->set('X-Experience-API-Consistent-Through', $now->format(\DateTime::ATOM));
+ $response->headers->set('Content-Type', 'application/json');
return $response;
}
@@ -103,11 +110,11 @@ final class StatementGetController
*
* @return JsonResponse|MultipartResponse
*/
- private function buildSingleStatementResponse(Statement $statement, $includeAttachments = false)
+ protected function buildSingleStatementResponse(Statement $statement, $includeAttachments = false)
{
$json = $this->statementSerializer->serializeStatement($statement);
- $response = new JsonResponse($json, 200, [], true);
+ $response = new Response($json, 200);
if ($includeAttachments) {
$response = $this->buildMultipartResponse($response, [$statement]);
@@ -124,11 +131,15 @@ final class StatementGetController
*
* @return JsonResponse|MultipartResponse
*/
- private function buildMultiStatementsResponse(array $statements, $includeAttachments = false)
+ protected function buildMultiStatementsResponse(array $statements, ParameterBag $query, $includeAttachments = false)
{
- $json = $this->statementResultSerializer->serializeStatementResult(new StatementResult($statements));
+ $moreUrlPath = $statements ? $this->generateMoreIrl($query) : null;
- $response = new JsonResponse($json, 200, [], true);
+ $json = $this->statementResultSerializer->serializeStatementResult(
+ new StatementResult($statements, $moreUrlPath)
+ );
+
+ $response = new Response($json, 200);
if ($includeAttachments) {
$response = $this->buildMultipartResponse($response, $statements);
@@ -142,7 +153,7 @@ final class StatementGetController
*
* @return MultipartResponse
*/
- private function buildMultipartResponse(JsonResponse $statementResponse, array $statements)
+ protected function buildMultipartResponse(JsonResponse $statementResponse, array $statements)
{
$attachmentsParts = [];
@@ -160,7 +171,7 @@ final class StatementGetController
*
* @throws BadRequestHttpException if the parameters does not comply with the xAPI specification
*/
- private function validate(ParameterBag $query)
+ protected function validate(ParameterBag $query)
{
$hasStatementId = $query->has('statementId');
$hasVoidedStatementId = $query->has('voidedStatementId');
@@ -185,4 +196,14 @@ final class StatementGetController
throw new BadRequestHttpException('Request must not contain statementId or voidedStatementId parameters, and also any other parameter besides "attachments" or "format".');
}
}
+
+ protected function generateMoreIrl(ParameterBag $query): IRL
+ {
+ $params = $query->all();
+ $params['cursor'] = empty($params['cursor']) ? 1 : $params['cursor'] + 1;
+
+ return IRL::fromString(
+ '/plugin/xapi/lrs.php/statements?'.http_build_query($params)
+ );
+ }
}
diff --git a/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementHeadController.php b/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementHeadController.php
new file mode 100644
index 0000000000..224a441850
--- /dev/null
+++ b/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementHeadController.php
@@ -0,0 +1,27 @@
+setContent('');
+ }
+}
diff --git a/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPostController.php b/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPostController.php
index 5937caa7fb..d426254173 100644
--- a/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPostController.php
+++ b/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPostController.php
@@ -11,22 +11,57 @@
namespace XApi\LrsBundle\Controller;
+use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
+use Xabbuh\XApi\Common\Exception\NotFoundException;
use Xabbuh\XApi\Model\Statement;
+use XApi\Repository\Api\StatementRepositoryInterface;
/**
* @author Jérôme Parmentier
*/
final class StatementPostController
{
- public function postStatement(Request $request, Statement $statement)
+ /**
+ * @var StatementRepositoryInterface
+ */
+ private $repository;
+
+ public function __construct(StatementRepositoryInterface $repository)
{
+ $this->repository = $repository;
}
- /**
- * @param Statement[] $statements
- */
- public function postStatements(Request $request, array $statements)
+ public function postStatements(Request $request, array $statements): JsonResponse
{
+ $statementsToStore = [];
+
+ /** @var Statement $statement */
+ foreach ($statements as $statement) {
+ if (null === $statementId = $statement->getId()) {
+ $statementsToStore[] = $statement;
+
+ continue;
+ }
+
+ try {
+ $existingStatement = $this->repository->findStatementById($statement->getId());
+
+ if (!$existingStatement->equals($statement)) {
+ throw new ConflictHttpException('The new statement is not equal to an existing statement with the same id.');
+ }
+ } catch (NotFoundException $e) {
+ $statementsToStore[] = $statement;
+ }
+ }
+
+ $uuids = [];
+
+ foreach ($statementsToStore as $statement) {
+ $uuids[] = $this->repository->storeStatement($statement, true)->getValue();
+ }
+
+ return new JsonResponse($uuids);
}
}
diff --git a/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPutController.php b/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPutController.php
index 4a01645b59..660f99b369 100644
--- a/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPutController.php
+++ b/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPutController.php
@@ -32,7 +32,7 @@ final class StatementPutController
$this->repository = $repository;
}
- public function putStatement(Request $request, Statement $statement)
+ public function putStatement(Request $request, Statement $statement): Response
{
if (null === $statementId = $request->query->get('statementId')) {
throw new BadRequestHttpException('Required statementId parameter is missing.');
@@ -55,6 +55,8 @@ final class StatementPutController
throw new ConflictHttpException('The new statement is not equal to an existing statement with the same id.');
}
} catch (NotFoundException $e) {
+ $statement = $statement->withId($id);
+
$this->repository->storeStatement($statement, true);
}
diff --git a/plugin/xapi/php-xapi/repository-doctrine-orm/src/StatementRepository.php b/plugin/xapi/php-xapi/repository-doctrine-orm/src/StatementRepository.php
index 9049a5e3b8..9d2bdd5ab5 100644
--- a/plugin/xapi/php-xapi/repository-doctrine-orm/src/StatementRepository.php
+++ b/plugin/xapi/php-xapi/repository-doctrine-orm/src/StatementRepository.php
@@ -12,6 +12,7 @@
namespace XApi\Repository\ORM;
use Doctrine\ORM\EntityRepository;
+use XApi\Repository\Doctrine\Mapping\Context;
use XApi\Repository\Doctrine\Mapping\Statement;
use XApi\Repository\Doctrine\Repository\Mapping\StatementRepository as BaseStatementRepository;
@@ -33,6 +34,25 @@ final class StatementRepository extends EntityRepository implements BaseStatemen
*/
public function findStatements(array $criteria)
{
+ if (!empty($criteria['registration'])) {
+ $context = $this->_em->getRepository(Context::class)->findOneBy([
+ 'registration' => $criteria['registration'],
+ ]);
+
+ unset(
+ $criteria['registration']
+ );
+
+ $criteria['context'] = $context;
+ }
+
+ unset(
+ $criteria['related_activities'],
+ $criteria['related_agents'],
+ $criteria['ascending'],
+ $criteria['limit']
+ );
+
return parent::findBy($criteria);
}
diff --git a/plugin/xapi/src/Entity/ActivityProfile.php b/plugin/xapi/src/Entity/ActivityProfile.php
new file mode 100644
index 0000000000..82f07523cc
--- /dev/null
+++ b/plugin/xapi/src/Entity/ActivityProfile.php
@@ -0,0 +1,93 @@
+id;
+ }
+
+ public function setId(int $id): ActivityProfile
+ {
+ $this->id = $id;
+
+ return $this;
+ }
+
+ public function getProfileId(): string
+ {
+ return $this->profileId;
+ }
+
+ public function setProfileId(string $profileId): ActivityProfile
+ {
+ $this->profileId = $profileId;
+
+ return $this;
+ }
+
+ public function getActivityId(): string
+ {
+ return $this->activityId;
+ }
+
+ public function setActivityId(string $activityId): ActivityProfile
+ {
+ $this->activityId = $activityId;
+
+ return $this;
+ }
+
+ public function getDocumentData(): array
+ {
+ return $this->documentData;
+ }
+
+ public function setDocumentData(array $documentData): ActivityProfile
+ {
+ $this->documentData = $documentData;
+
+ return $this;
+ }
+}
diff --git a/plugin/xapi/src/Entity/Repository/ToolLaunchRepository.php b/plugin/xapi/src/Entity/Repository/ToolLaunchRepository.php
index 27981e1f42..0446c9fc09 100644
--- a/plugin/xapi/src/Entity/Repository/ToolLaunchRepository.php
+++ b/plugin/xapi/src/Entity/Repository/ToolLaunchRepository.php
@@ -53,7 +53,13 @@ class ToolLaunchRepository extends EntityRepository
}
if ($filteredForStudent) {
- $qb->leftJoin(CLpItem::class, 'lpi', Join::WITH, 'tl.id = lpi.path')
+ $qb
+ ->leftJoin(
+ CLpItem::class,
+ 'lpi',
+ Join::WITH,
+ "tl.id = lpi.path AND tl.course = lpi.cId AND lpi.itemType = 'xapi'"
+ )
->andWhere($qb->expr()->isNull('lpi.path'));
}
diff --git a/plugin/xapi/src/Lrs/AboutController.php b/plugin/xapi/src/Lrs/AboutController.php
new file mode 100644
index 0000000000..084f2ddae6
--- /dev/null
+++ b/plugin/xapi/src/Lrs/AboutController.php
@@ -0,0 +1,30 @@
+ [
+ '1.0.3',
+ '1.0.2',
+ '1.0.1',
+ '1.0.0',
+ ],
+ ];
+
+ return JsonResponse::create($json);
+ }
+}
diff --git a/plugin/xapi/src/Lrs/ActivitiesProfileController.php b/plugin/xapi/src/Lrs/ActivitiesProfileController.php
new file mode 100644
index 0000000000..bf12e82f0f
--- /dev/null
+++ b/plugin/xapi/src/Lrs/ActivitiesProfileController.php
@@ -0,0 +1,78 @@
+httpRequest->query->get('profileId');
+ $activityId = $this->httpRequest->query->get('activityId');
+
+ $em = \Database::getManager();
+ $profileRepo = $em->getRepository(ActivityProfile::class);
+
+ /** @var ActivityProfile $activityProfile */
+ $activityProfile = $profileRepo->findOneBy(
+ [
+ 'profileId' => $profileId,
+ 'activityId' => $activityId,
+ ]
+ );
+
+ if (empty($activityProfile)) {
+ return Response::create(null, Response::HTTP_NO_CONTENT);
+ }
+
+ return Response::create(
+ json_encode($activityProfile->getDocumentData())
+ );
+ }
+
+ public function head(): Response
+ {
+ return $this->get()->setContent('');
+ }
+
+ public function put(): Response
+ {
+ $profileId = $this->httpRequest->query->get('profileId');
+ $activityId = $this->httpRequest->query->get('activityId');
+ $documentData = $this->httpRequest->getContent();
+
+ $em = \Database::getManager();
+ $profileRepo = $em->getRepository(ActivityProfile::class);
+
+ /** @var ActivityProfile $activityProfile */
+ $activityProfile = $profileRepo->findOneBy(
+ [
+ 'profileId' => $profileId,
+ 'activityId' => $activityId,
+ ]
+ );
+
+ if (empty($activityProfile)) {
+ $activityProfile = new ActivityProfile();
+ $activityProfile
+ ->setProfileId($profileId)
+ ->setActivityId($activityId);
+ }
+
+ $activityProfile->setDocumentData(json_decode($documentData, true));
+
+ $em->persist($activityProfile);
+ $em->flush();
+
+ return Response::create(null, Response::HTTP_NO_CONTENT);
+ }
+}
diff --git a/plugin/xapi/src/Lrs/ActivitiesController.php b/plugin/xapi/src/Lrs/ActivitiesStateController.php
similarity index 89%
rename from plugin/xapi/src/Lrs/ActivitiesController.php
rename to plugin/xapi/src/Lrs/ActivitiesStateController.php
index 6e9ea84906..6d05fc7b86 100644
--- a/plugin/xapi/src/Lrs/ActivitiesController.php
+++ b/plugin/xapi/src/Lrs/ActivitiesStateController.php
@@ -1,9 +1,8 @@
get()->setContent('');
+ }
+
+ public function post(): Response
+ {
+ return $this->put();
+ }
+
+ public function put(): Response
{
$activityId = $this->httpRequest->query->get('activityId');
$agent = $this->httpRequest->query->get('agent');
diff --git a/plugin/xapi/src/Lrs/BaseController.php b/plugin/xapi/src/Lrs/BaseController.php
index ac85ccdc77..865b2b71b6 100644
--- a/plugin/xapi/src/Lrs/BaseController.php
+++ b/plugin/xapi/src/Lrs/BaseController.php
@@ -21,8 +21,8 @@ abstract class BaseController
/**
* BaseController constructor.
*/
- public function __construct()
+ public function __construct(Request $httpRequest)
{
- $this->httpRequest = Request::createFromGlobals();
+ $this->httpRequest = $httpRequest;
}
}
diff --git a/plugin/xapi/src/Lrs/LrsRequest.php b/plugin/xapi/src/Lrs/LrsRequest.php
index 74ab274276..f409f61943 100644
--- a/plugin/xapi/src/Lrs/LrsRequest.php
+++ b/plugin/xapi/src/Lrs/LrsRequest.php
@@ -5,10 +5,14 @@
namespace Chamilo\PluginBundle\XApi\Lrs;
use Chamilo\PluginBundle\Entity\XApi\LrsAuth;
+use Database;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
-use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Xabbuh\XApi\Common\Exception\AccessDeniedException;
+use Xabbuh\XApi\Common\Exception\XApiException;
/**
* Class LrsRequest.
@@ -32,56 +36,105 @@ class LrsRequest
public function send()
{
- $this->validAuth();
+ try {
+ $this->alternateRequestSyntax();
+
+ $controllerName = $this->getControllerName();
+ $methodName = $this->getMethodName();
+
+ $response = $this->generateResponse($controllerName, $methodName);
+ } catch (XApiException $xApiException) {
+ $response = HttpResponse::create('', HttpResponse::HTTP_BAD_REQUEST);
+ } catch (HttpException $httpException) {
+ $response = HttpResponse::create(
+ $httpException->getMessage(),
+ $httpException->getStatusCode()
+ );
+ } catch (\Exception $exception) {
+ $response = HttpResponse::create($exception->getMessage(), HttpResponse::HTTP_BAD_REQUEST);
+ }
- $version = $this->request->headers->get('X-Experience-API-Version');
+ $response->headers->set('X-Experience-API-Version', '1.0.3');
- if (null === $version) {
- throw new BadRequestHttpException('The "X-Experience-API-Version" header is required.');
+ $response->send();
+ }
+
+ /**
+ * @throws \Xabbuh\XApi\Common\Exception\AccessDeniedException
+ */
+ private function validateAuth(): bool
+ {
+ if (!$this->request->headers->has('Authorization')) {
+ throw new AccessDeniedException();
}
- if (!$this->isValidVersion($version)) {
- throw new BadRequestHttpException("The xAPI version \"$version\" is not supported.");
+ $authHeader = $this->request->headers->get('Authorization');
+
+ $parts = explode('Basic ', $authHeader, 2);
+
+ if (empty($parts[1])) {
+ throw new AccessDeniedException();
}
- $controllerName = $this->getControllerName();
- $methodName = $this->getMethodName();
+ $authDecoded = base64_decode($parts[1]);
- if ($controllerName
- && class_exists($controllerName)
- && method_exists($controllerName, $methodName)
- ) {
- /** @var HttpResponse $response */
- $response = call_user_func([new $controllerName(), $methodName]);
- } else {
- $response = HttpResponse::create('Not Found', HttpResponse::HTTP_NOT_FOUND);
+ $parts = explode(':', $authDecoded, 2);
+
+ if (empty($parts) || count($parts) !== 2) {
+ throw new AccessDeniedException();
}
- $response->headers->set('X-Experience-API-Version', '1.0.3');
+ list($username, $password) = $parts;
- $response->send();
+ $auth = Database::getManager()
+ ->getRepository(LrsAuth::class)
+ ->findOneBy(
+ ['username' => $username, 'password' => $password, 'enabled' => true]
+ );
+
+ if (null == $auth) {
+ throw new AccessDeniedException();
+ }
+
+ return true;
}
- /**
- * @return string|null
- */
- private function getControllerName()
+ private function validateVersion()
+ {
+ $version = $this->request->headers->get('X-Experience-API-Version');
+
+ if (null === $version) {
+ throw new BadRequestHttpException('The "X-Experience-API-Version" header is required.');
+ }
+
+ if (preg_match('/^1\.0(?:\.\d+)?$/', $version)) {
+ if ('1.0' === $version) {
+ $this->request->headers->set('X-Experience-API-Version', '1.0.0');
+ }
+
+ return;
+ }
+
+ throw new BadRequestHttpException("The xAPI version \"$version\" is not supported.");
+ }
+
+ private function getControllerName(): ?string
{
$segments = explode('/', $this->request->getPathInfo());
+ $segments = array_filter($segments);
+ $segments = array_values($segments);
- if (empty($segments[1])) {
- return null;
+ if (empty($segments)) {
+ throw new BadRequestHttpException('Bad request');
}
- $controllerName = ucfirst($segments[1]).'Controller';
+ $segments = array_map('ucfirst', $segments);
+ $controllerName = implode('', $segments).'Controller';
return "Chamilo\\PluginBundle\\XApi\Lrs\\$controllerName";
}
- /**
- * @return string
- */
- private function getMethodName()
+ private function getMethodName(): string
{
$method = $this->request->getMethod();
@@ -89,60 +142,80 @@ class LrsRequest
}
/**
- * @param string $version
- *
- * @return bool
+ * @throws \Xabbuh\XApi\Common\Exception\AccessDeniedException
*/
- private function isValidVersion($version)
+ private function generateResponse(string $controllerName, string $methodName): HttpResponse
{
- if (preg_match('/^1\.0(?:\.\d+)?$/', $version)) {
- if ('1.0' === $version) {
- $this->request->headers->set('X-Experience-API-Version', '1.0.0');
- }
+ if (!class_exists($controllerName)
+ || !method_exists($controllerName, $methodName)
+ ) {
+ throw new NotFoundHttpException();
+ }
- return true;
+ if ($controllerName !== AboutController::class) {
+ $this->validateAuth();
+ $this->validateVersion();
}
- return false;
+ /** @var HttpResponse $response */
+ $response = call_user_func(
+ [
+ new $controllerName($this->request),
+ $methodName,
+ ]
+ );
+
+ return $response;
}
- /**
- * @return bool
- */
- private function validAuth()
+ private function alternateRequestSyntax()
{
- if (!$this->request->headers->has('Authorization')) {
- throw new AccessDeniedHttpException();
+ if ('POST' !== $this->request->getMethod()) {
+ return;
}
- $authHeader = $this->request->headers->get('Authorization');
-
- $parts = explode('Basic ', $authHeader, 2);
+ if (null === $method = $this->request->query->get('method')) {
+ return;
+ }
- if (empty($parts[1])) {
- throw new AccessDeniedHttpException();
+ if ($this->request->query->count() > 1) {
+ throw new BadRequestHttpException('Including other query parameters than "method" is not allowed. You have to send them as POST parameters inside the request body.');
}
- $authDecoded = base64_decode($parts[1]);
+ $this->request->setMethod($method);
+ $this->request->query->remove('method');
- $parts = explode(':', $authDecoded, 2);
+ if (null !== $content = $this->request->request->get('content')) {
+ $this->request->request->remove('content');
- if (empty($parts) || count($parts) !== 2) {
- throw new AccessDeniedHttpException();
+ $this->request->initialize(
+ $this->request->query->all(),
+ $this->request->request->all(),
+ $this->request->attributes->all(),
+ $this->request->cookies->all(),
+ $this->request->files->all(),
+ $this->request->server->all(),
+ $content
+ );
}
- list($username, $password) = $parts;
-
- $auth = \Database::getManager()
- ->getRepository(LrsAuth::class)
- ->findOneBy(
- ['username' => $username, 'password' => $password, 'enabled' => true]
- );
+ $headerNames = [
+ 'Authorization',
+ 'X-Experience-API-Version',
+ 'Content-Type',
+ 'Content-Length',
+ 'If-Match',
+ 'If-None-Match',
+ ];
+
+ foreach ($this->request->request as $key => $value) {
+ if (in_array($key, $headerNames, true)) {
+ $this->request->headers->set($key, $value);
+ } else {
+ $this->request->query->set($key, $value);
+ }
- if (null == $auth) {
- throw new AccessDeniedHttpException();
+ $this->request->request->remove($key);
}
-
- return true;
}
}
diff --git a/plugin/xapi/src/Lrs/StatementsController.php b/plugin/xapi/src/Lrs/StatementsController.php
index dec9c97bd9..517a51a545 100644
--- a/plugin/xapi/src/Lrs/StatementsController.php
+++ b/plugin/xapi/src/Lrs/StatementsController.php
@@ -4,9 +4,16 @@
namespace Chamilo\PluginBundle\XApi\Lrs;
+use Symfony\Component\HttpFoundation\Response;
use Xabbuh\XApi\Model\Statement;
+use Xabbuh\XApi\Serializer\Symfony\ActorSerializer;
use Xabbuh\XApi\Serializer\Symfony\Serializer;
+use Xabbuh\XApi\Serializer\Symfony\SerializerFactory;
+use XApi\LrsBundle\Controller\StatementGetController;
+use XApi\LrsBundle\Controller\StatementHeadController;
+use XApi\LrsBundle\Controller\StatementPostController;
use XApi\LrsBundle\Controller\StatementPutController;
+use XApi\LrsBundle\Model\StatementsFilterFactory;
use XApi\Repository\Doctrine\Mapping\Statement as StatementEntity;
use XApi\Repository\Doctrine\Repository\StatementRepository;
use XApiPlugin;
@@ -18,6 +25,48 @@ use XApiPlugin;
*/
class StatementsController extends BaseController
{
+ public function get(): Response
+ {
+ $pluginEm = XApiPlugin::getEntityManager();
+
+ $serializer = Serializer::createSerializer();
+ $factory = new SerializerFactory($serializer);
+
+ $getStatementController = new StatementGetController(
+ new StatementRepository(
+ $pluginEm->getRepository(StatementEntity::class)
+ ),
+ $factory->createStatementSerializer(),
+ $factory->createStatementResultSerializer(),
+ new StatementsFilterFactory(
+ new ActorSerializer($serializer)
+ )
+ );
+
+ return $getStatementController->getStatement($this->httpRequest);
+ }
+
+ public function head(): Response
+ {
+ $pluginEm = XApiPlugin::getEntityManager();
+
+ $serializer = Serializer::createSerializer();
+ $factory = new SerializerFactory($serializer);
+
+ $headStatementController = new StatementHeadController(
+ new StatementRepository(
+ $pluginEm->getRepository(StatementEntity::class)
+ ),
+ $factory->createStatementSerializer(),
+ $factory->createStatementResultSerializer(),
+ new StatementsFilterFactory(
+ new ActorSerializer($serializer)
+ )
+ );
+
+ return $headStatementController->getStatement($this->httpRequest);
+ }
+
/**
* @return \Symfony\Component\HttpFoundation\Response
*/
@@ -38,15 +87,38 @@ class StatementsController extends BaseController
return $putStatementController->putStatement($this->httpRequest, $statement);
}
- /**
- * @param string $content
- *
- * @return \Xabbuh\XApi\Model\Statement
- */
- private function deserializeStatement($content)
+ public function post(): Response
{
- $serializer = Serializer::createSerializer();
+ $pluginEm = XApiPlugin::getEntityManager();
+
+ $postStatementController = new StatementPostController(
+ new StatementRepository(
+ $pluginEm->getRepository(StatementEntity::class)
+ )
+ );
+
+ $content = $this->httpRequest->getContent();
+
+ if (substr($content, 0, 1) !== '[') {
+ $content = "[$content]";
+ }
+
+ $statements = $this->deserializeStatements($content);
+
+ return $postStatementController->postStatements($this->httpRequest, $statements);
+ }
+
+ private function deserializeStatement(string $content = ''): Statement
+ {
+ $factory = new SerializerFactory(Serializer::createSerializer());
+
+ return $factory->createStatementSerializer()->deserializeStatement($content);
+ }
+
+ private function deserializeStatements(string $content = ''): array
+ {
+ $factory = new SerializerFactory(Serializer::createSerializer());
- return $serializer->deserialize($content, Statement::class, 'json');
+ return $factory->createStatementSerializer()->deserializeStatements($content);
}
}
diff --git a/plugin/xapi/src/XApiPlugin.php b/plugin/xapi/src/XApiPlugin.php
index 13dc2a1f93..0c2cd5ca86 100644
--- a/plugin/xapi/src/XApiPlugin.php
+++ b/plugin/xapi/src/XApiPlugin.php
@@ -2,6 +2,8 @@
/* For licensing terms, see /license.txt */
+use Chamilo\PluginBundle\Entity\XApi\ActivityProfile;
+use Chamilo\PluginBundle\Entity\XApi\ActivityState;
use Chamilo\PluginBundle\Entity\XApi\Cmi5Item;
use Chamilo\PluginBundle\Entity\XApi\LrsAuth;
use Chamilo\PluginBundle\Entity\XApi\SharedStatement;
@@ -90,6 +92,8 @@ class XApiPlugin extends Plugin implements HookPluginInterface
'xapi_tool_launch',
'xapi_lrs_auth',
'xapi_cmi5_item',
+ 'xapi_activity_state',
+ 'xapi_activity_profile',
'xapi_attachment',
'xapi_object',
@@ -154,6 +158,8 @@ class XApiPlugin extends Plugin implements HookPluginInterface
$schemaTool = new SchemaTool($em);
$schemaTool->dropSchema(
[
+ $em->getClassMetadata(ActivityProfile::class),
+ $em->getClassMetadata(ActivityState::class),
$em->getClassMetadata(SharedStatement::class),
$em->getClassMetadata(ToolLaunch::class),
$em->getClassMetadata(LrsAuth::class),
@@ -495,6 +501,8 @@ class XApiPlugin extends Plugin implements HookPluginInterface
$em->getClassMetadata(ToolLaunch::class),
$em->getClassMetadata(LrsAuth::class),
$em->getClassMetadata(Cmi5Item::class),
+ $em->getClassMetadata(ActivityState::class),
+ $em->getClassMetadata(ActivityProfile::class),
]
);
diff --git a/plugin/xapi/tool_import.php b/plugin/xapi/tool_import.php
index 9aaf574cb7..da878d9a4f 100644
--- a/plugin/xapi/tool_import.php
+++ b/plugin/xapi/tool_import.php
@@ -127,7 +127,7 @@ if ($frmActivity->validate()) {
$toolLaunch
->setLrsUrl($values['lrs_url'])
->setLrsAuthUsername($values['lrs_auth_username'])
- ->setLrsAuthUsername($values['lrs_auth_password']);
+ ->setLrsAuthPassword($values['lrs_auth_password']);
}
$em = Database::getManager();
diff --git a/src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/Basic.php b/src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/Basic.php
index 94011793a6..cfd827728d 100644
--- a/src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/Basic.php
+++ b/src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/Basic.php
@@ -158,6 +158,11 @@ class Basic extends Toolbar
$this->defaultPlugins = array_unique(array_merge($this->defaultPlugins, $plugins));
+ $editorSettings = api_get_configuration_value('editor_settings');
+ if (!empty($editorSettings) && isset($editorSettings['config']) && !empty($editorSettings['config'])) {
+ $config = array_merge($config, $editorSettings['config']);
+ }
+
parent::__construct($toolbar, $config, $prefix);
}