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