Merge branch '1.11.x' of github.com:chamilo/chamilo-lms into 1.11.x

pull/3973/head
Yannick Warnier 5 years ago
commit f6d961b51a
  1. 18
      main/cron/import_csv.php
  2. 29
      main/exercise/comparative_group_report.php
  3. 59
      main/exercise/question_stats.php
  4. 106
      main/exercise/stats.php
  5. 8
      main/inc/ajax/exercise.ajax.php
  6. 39
      main/inc/lib/agenda.lib.php
  7. 28
      main/inc/lib/exercise.lib.php
  8. 2
      main/inc/lib/online.inc.php
  9. 5
      main/install/configuration.dist.php
  10. 4
      main/lp/learnpathItem.class.php
  11. 21
      main/mySpace/student_follow_export.php
  12. 4
      plugin/exercise_signature/lib/ExerciseSignature.php
  13. 2
      plugin/studentfollowup/StudentFollowUpPlugin.php
  14. 1
      plugin/xapi/.htaccess
  15. 1
      plugin/xapi/README.md
  16. 51
      plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementGetController.php
  17. 27
      plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementHeadController.php
  18. 45
      plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPostController.php
  19. 4
      plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPutController.php
  20. 20
      plugin/xapi/php-xapi/repository-doctrine-orm/src/StatementRepository.php
  21. 93
      plugin/xapi/src/Entity/ActivityProfile.php
  22. 8
      plugin/xapi/src/Entity/Repository/ToolLaunchRepository.php
  23. 30
      plugin/xapi/src/Lrs/AboutController.php
  24. 78
      plugin/xapi/src/Lrs/ActivitiesProfileController.php
  25. 30
      plugin/xapi/src/Lrs/ActivitiesStateController.php
  26. 4
      plugin/xapi/src/Lrs/BaseController.php
  27. 205
      plugin/xapi/src/Lrs/LrsRequest.php
  28. 88
      plugin/xapi/src/Lrs/StatementsController.php
  29. 8
      plugin/xapi/src/XApiPlugin.php
  30. 2
      plugin/xapi/tool_import.php
  31. 5
      src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/Basic.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']."' ");

@ -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 = '<a href="exercise_report.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'">'.
Display:: return_icon(
'back.png',
get_lang('GoBackToQuestionList'),
'',
ICON_SIZE_MEDIUM
)
.'</a>';
$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();

@ -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[] = '<script>
$(function() {
$("#export-xls").bind("click", function(e) {
e.preventDefault();
var input = $("<input>", {
type: "hidden",
name: "export_xls",
value: "1"
});
$("#search_form").append(input);
$("#search_form").submit();
});
$("#search_form_searchSubmit").bind("click", function(e) {
e.preventDefault();
if ($("input[name=\"export_xls\"]").length > 0) {
$("input[name=\"export_xls\"]").remove();
}
$("#search_form").submit();
});
});
</script>';
Display::display_header($nameTools, get_lang('Exercise'));
$actions = '<a href="exercise_report.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'">'.
Display:: return_icon(
'back.png',
get_lang('GoBackToQuestionList'),
'',
ICON_SIZE_MEDIUM
)
.'</a>';
$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();

@ -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);

@ -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]);
}
}

@ -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,

@ -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 .= '<script>
function setRemoveLink(dataContext) {
var removeLink = $("<a>", {
html: "&nbsp;'.addslashes($iconDelete).'",
href: "#",
click: function(e) {
e.preventDefault();
dataContext.parent().remove();
}
});
dataContext.append(removeLink);
}
$(function() {
$("#input_file_upload").bind("fileuploaddone", function (e, data) {
$.each(data.result.files, function (index, file) {
@ -225,8 +237,10 @@ class ExerciseLib
type: "hidden",
name: "uploadChoice['.$questionId.'][]",
value: file.name
})
});
$(data.context.children()[index]).parent().append(input);
// set the remove link
setRemoveLink($(data.context.children()[index]).parent());
}
});
});
@ -235,17 +249,25 @@ class ExerciseLib
// Set default values
if (!empty($answer)) {
$userWebpath = UserManager::getUserPathById(api_get_user_id(), 'web').'my_files'.'/upload_answer/'.$exe_id.'/'.$questionId.'/';
$userSyspath = UserManager::getUserPathById(api_get_user_id(), 'system').'my_files'.'/upload_answer/'.$exe_id.'/'.$questionId.'/';
$filesNames = explode('|', $answer);
$icon = Display::return_icon('file_txt.gif');
$default = '';
foreach ($filesNames as $fileName) {
$fileName = Security::remove_XSS($fileName);
$default .= '<a target="_blank" class="panel-image" href="'.$userWebpath.$fileName.'"><div class="row"><div class="col-sm-4">'.$icon.'</div><div class="col-sm-5 file_name">'.$fileName.'</div><input type="hidden" name="uploadChoice['.$questionId.'][]" value="'.$fileName.'"></div></a>';
if (file_exists($userSyspath.$fileName)) {
$default .= '<a target="_blank" class="panel-image" href="'.$userWebpath.$fileName.'"><div class="row"><div class="col-sm-4">'.$icon.'</div><div class="col-sm-5 file_name">'.$fileName.'</div><input type="hidden" name="uploadChoice['.$questionId.'][]" value="'.$fileName.'"><div class="col-sm-3"></div></div></a>';
}
}
$s .= '<script>
$(function() {
if ($("#files").length > 0) {
$("#files").html("'.addslashes($default).'");
$("#files").html("'.addslashes($default).'");
var links = $("#files").children();
links.each(function(index) {
var dataContext = $(links[index]).find(".row");
setRemoveLink(dataContext);
});
}
});
</script>';

@ -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;
}

@ -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

@ -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)) {

@ -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);
}
}

@ -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;
}

@ -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'];

@ -0,0 +1 @@
AcceptPathInfo On

@ -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;
```

@ -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 <jerome.parmentier@acensi.fr>
*/
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)
);
}
}

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the xAPI package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace XApi\LrsBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class StatementHeadController extends StatementGetController
{
/**
* @throws BadRequestHttpException if the query parameters does not comply with xAPI specification
*
* @return Response
*/
public function getStatement(Request $request)
{
return parent::getStatement($request)->setContent('');
}
}

@ -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 <jerome.parmentier@acensi.fr>
*/
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);
}
}

@ -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);
}

@ -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);
}

@ -0,0 +1,93 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\XApi;
use Doctrine\ORM\Mapping as ORM;
/**
* Class ActivityProfile.
*
* @package Chamilo\PluginBundle\Entity\XApi
*
* @ORM\Table(name="xapi_activity_profile")
* @ORM\Entity()
*/
class ActivityProfile
{
/**
* @var int
*
* @ORM\Column(type="integer", name="id")
* @ORM\Id()
* @ORM\GeneratedValue()
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="profile_id", type="string")
*/
private $profileId;
/**
* @var string
*
* @ORM\Column(name="activity_id", type="string")
*/
private $activityId;
/**
* @var array
*
* @ORM\Column(name="document_data", type="json")
*/
private $documentData;
public function getId(): int
{
return $this->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;
}
}

@ -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'));
}

@ -0,0 +1,30 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Lrs;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
/**
* Class AboutController.
*
* @package Chamilo\PluginBundle\XApi\Lrs
*/
class AboutController extends BaseController
{
public function get(): Response
{
$json = [
'version' => [
'1.0.3',
'1.0.2',
'1.0.1',
'1.0.0',
],
];
return JsonResponse::create($json);
}
}

@ -0,0 +1,78 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Lrs;
use Chamilo\PluginBundle\Entity\XApi\ActivityProfile;
use Symfony\Component\HttpFoundation\Response;
/**
* Class ActivitiesProfileController.
*
* @package Chamilo\PluginBundle\XApi\Lrs
*/
class ActivitiesProfileController extends BaseController
{
public function get(): Response
{
$profileId = $this->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);
}
}

@ -1,9 +1,8 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Lrs;
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\XApi\ActivityState;
use Database;
use Symfony\Component\HttpFoundation\JsonResponse;
@ -12,16 +11,13 @@ use Xabbuh\XApi\Model\Actor;
use Xabbuh\XApi\Serializer\Symfony\Serializer;
/**
* Class ActivitiesController.
* Class ActivitiesStateController.
*
* @package Chamilo\PluginBundle\XApi\Lrs
*/
class ActivitiesController extends BaseController
class ActivitiesStateController extends BaseController
{
/**
* @return \Symfony\Component\HttpFoundation\JsonResponse
*/
public function get()
public function get(): Response
{
$serializer = Serializer::createSerializer();
@ -70,13 +66,17 @@ class ActivitiesController extends BaseController
return JsonResponse::create($documentData);
}
/**
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function put()
public function head(): Response
{
return $this->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');

@ -21,8 +21,8 @@ abstract class BaseController
/**
* BaseController constructor.
*/
public function __construct()
public function __construct(Request $httpRequest)
{
$this->httpRequest = Request::createFromGlobals();
$this->httpRequest = $httpRequest;
}
}

@ -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;
}
}

@ -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);
}
}

@ -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),
]
);

@ -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();

@ -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);
}

Loading…
Cancel
Save