Merge pull request #3377 from chamilo/whispeak

Update API requests for Whispeak plugin
pull/3378/head
Angel Fernando Quiroz Campos 5 years ago committed by GitHub
commit 4399ccd5d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      main/auth/lostPassword.php
  2. 12
      main/inc/ajax/exercise.ajax.php
  3. 23
      main/inc/lib/exercise.lib.php
  4. 3
      main/inc/lib/hook/HookCreateUser.php
  5. 2
      main/inc/lib/hook/interfaces/HookConditionalLoginObserverInterface.php
  6. 4
      main/inc/lib/hook/interfaces/HookMyStudentsLpTrackingObserverInterface.php
  7. 4
      main/inc/lib/hook/interfaces/HookMyStudentsQuizTrackingObserverInterface.php
  8. 36
      main/inc/lib/lp_item.lib.php
  9. 5
      main/inc/lib/userportal.lib.php
  10. 40
      main/lp/learnpath.class.php
  11. 5
      main/lp/learnpathItem.class.php
  12. 8
      main/lp/lp_ajax_switch_item.php
  13. 9
      main/lp/lp_content.php
  14. 5
      main/lp/lp_controller.php
  15. 11
      main/lp/lp_view.php
  16. 183
      main/mySpace/myStudents.php
  17. 345
      plugin/whispeakauth/Controller/AuthenticationController.php
  18. 66
      plugin/whispeakauth/Controller/BaseController.php
  19. 98
      plugin/whispeakauth/Controller/EnrollmentController.php
  20. 145
      plugin/whispeakauth/Entity/LogEvent.php
  21. 84
      plugin/whispeakauth/Entity/LogEventLp.php
  22. 84
      plugin/whispeakauth/Entity/LogEventQuiz.php
  23. 12
      plugin/whispeakauth/README.md
  24. 188
      plugin/whispeakauth/Request/ApiRequest.php
  25. 775
      plugin/whispeakauth/WhispeakAuthPlugin.php
  26. 61
      plugin/whispeakauth/WhispeakConditionalLoginHook.php
  27. 63
      plugin/whispeakauth/WhispeakMyStudentsLpTrackingHook.php
  28. 81
      plugin/whispeakauth/WhispeakMyStudentsQuizTrackingHook.php
  29. 128
      plugin/whispeakauth/admin.php
  30. 142
      plugin/whispeakauth/ajax/authentify_password.php
  31. 124
      plugin/whispeakauth/ajax/record_audio.php
  32. 53
      plugin/whispeakauth/assets/js/RecordAudio.js
  33. 29
      plugin/whispeakauth/authentify.php
  34. 79
      plugin/whispeakauth/authentify_password.php
  35. 29
      plugin/whispeakauth/enrollment.php
  36. 28
      plugin/whispeakauth/lang/english.php
  37. 49
      plugin/whispeakauth/lang/french.php
  38. 28
      plugin/whispeakauth/lang/spanish.php
  39. 44
      plugin/whispeakauth/view/authentify_password.html.twig
  40. 34
      plugin/whispeakauth/view/authentify_recorder.html.twig
  41. 45
      plugin/whispeakauth/view/quiz_question.html.twig
  42. 62
      plugin/whispeakauth/view/record_audio.html.twig

@ -115,6 +115,10 @@ if ($form->validate()) {
exit;
}
if ('true' === api_get_plugin_setting('whispeakauth', WhispeakAuthPlugin::SETTING_ENABLE)) {
WhispeakAuthPlugin::deleteEnrollment($user['uid']);
}
$passwordEncryption = api_get_configuration_value('password_encryption');
if ($passwordEncryption === 'none') {

@ -447,6 +447,18 @@ switch ($action) {
exit;
}
if (WhispeakAuthPlugin::questionRequireAuthentify($question_id)) {
if ($objExercise->type == ONE_PER_PAGE) {
echo 'one_per_page';
break;
}
echo 'ok';
break;
} else {
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_QUIZ_QUESTION);
}
// Getting information of the current exercise.
$exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
$exercise_id = $exercise_stat_info['exe_exo_id'];

@ -63,6 +63,8 @@ class ExerciseLib
return false;
}
$questionRequireAuth = WhispeakAuthPlugin::questionRequireAuthentify($questionId);
if ($exercise->getFeedbackType() != EXERCISE_FEEDBACK_TYPE_END) {
$show_comment = false;
}
@ -93,6 +95,13 @@ class ExerciseLib
}
echo $titleToDisplay;
}
if ($questionRequireAuth) {
WhispeakAuthPlugin::quizQuestionAuthentify($questionId, $exercise);
return false;
}
if (!empty($questionDescription) && $answerType != READING_COMPREHENSION) {
echo Display::div(
$questionDescription,
@ -1497,6 +1506,13 @@ HTML;
}
echo $objQuestionTmp->getTitleToDisplay($current_item);
}
if ($questionRequireAuth) {
WhispeakAuthPlugin::quizQuestionAuthentify($questionId, $exercise);
return false;
}
//@todo I need to the get the feedback type
echo <<<HOTSPOT
<input type="hidden" name="hidden_hotspot_id" value="$questionId" />
@ -1567,6 +1583,13 @@ HOTSPOT;
}
echo $objQuestionTmp->getTitleToDisplay($current_item);
}
if ($questionRequireAuth) {
WhispeakAuthPlugin::quizQuestionAuthentify($questionId, $exercise);
return false;
}
echo '
<input type="hidden" name="hidden_hotspot_id" value="'.$questionId.'" />
<div class="exercise_questions">

@ -25,8 +25,9 @@ class HookCreateUser extends HookEvent implements HookCreateUserEventInterface
*/
public function notifyCreateUser($type)
{
/** @var \HookCreateUserObserverInterface $observer */
$this->eventData['type'] = $type;
/** @var HookCreateUserObserverInterface $observer */
foreach ($this->observers as $observer) {
$observer->hookCreateUser($this);
}

@ -16,6 +16,8 @@ interface HookConditionalLoginObserverInterface extends HookObserverInterface
* </code>
* conditional_function returns false to redirect to the url and returns true to continue with the classical login.
*
* @param HookConditionalLoginEventInterface $hook
*
* @return array
*/
public function hookConditionalLogin(HookConditionalLoginEventInterface $hook);

@ -15,6 +15,8 @@ interface HookMyStudentsLpTrackingObserverInterface extends HookObserverInterfac
* ]
* </code>.
*
* @param HookMyStudentsLpTrackingEventInterface $hook
*
* @return array
*/
public function trackingHeader(HookMyStudentsLpTrackingEventInterface $hook);
@ -28,6 +30,8 @@ interface HookMyStudentsLpTrackingObserverInterface extends HookObserverInterfac
* ]
* </code>.
*
* @param HookMyStudentsLpTrackingEventInterface $hook
*
* @return array
*/
public function trackingContent(HookMyStudentsLpTrackingEventInterface $hook);

@ -15,6 +15,8 @@ interface HookMyStudentsQuizTrackingObserverInterface extends HookObserverInterf
* ]
* </code>.
*
* @param HookMyStudentsQuizTrackingEventInterface $hook
*
* @return array
*/
public function trackingHeader(HookMyStudentsQuizTrackingEventInterface $hook);
@ -28,6 +30,8 @@ interface HookMyStudentsQuizTrackingObserverInterface extends HookObserverInterf
* ]
* </code>.
*
* @param HookMyStudentsQuizTrackingEventInterface $hook
*
* @return array
*/
public function trackingContent(HookMyStudentsQuizTrackingEventInterface $hook);

@ -109,4 +109,40 @@ class LpItem
Database::query($sql);
}
}
/**
* Create extra field for learning path item.
*
* @param string $variable
* @param int $fieldType
* @param string $displayText
* @param string|null $default Optional.
* @param bool $changeable Optional.
* @param bool $visibleToSelf Optional.
* @param bool $visibleToOthers Optional.
*
* @return bool|int
*/
public static function createExtraField(
$variable,
$fieldType,
$displayText,
$default = null,
$changeable = false,
$visibleToSelf = false,
$visibleToOthers = false
) {
$extraField = new ExtraField('lp_item');
$params = [
'variable' => $variable,
'field_type' => $fieldType,
'display_text' => $displayText,
'default_value' => $default,
'changeable' => $changeable,
'visible_to_self' => $visibleToSelf,
'visible_to_others' => $visibleToOthers,
];
return $extraField->save($params);
}
}

@ -952,7 +952,10 @@ class IndexManager
];
}
if (true === api_get_configuration_value('whispeak_auth_enabled')) {
if (
true === api_get_configuration_value('whispeak_auth_enabled') &&
!WhispeakAuthPlugin::checkUserIsEnrolled($userId)
) {
$itemTitle = WhispeakAuthPlugin::create()->get_title();
$items[] = [

@ -5454,7 +5454,10 @@ class learnpath
$item_type = $this->items[$this->current]->get_type();
if (($type == 2 && $item_type != 'sco') ||
($type == 3 && $item_type != 'au') ||
($type == 1 && $item_type != TOOL_QUIZ && $item_type != TOOL_HOTPOTATOES)
(
$type == 1 && $item_type != TOOL_QUIZ && $item_type != TOOL_HOTPOTATOES &&
WhispeakAuthPlugin::isAllowedToSaveLpItem($this->current)
)
) {
if ($debug) {
error_log('item type: '.$item_type);
@ -7624,6 +7627,11 @@ class learnpath
}
}
if ('edit' === $action) {
$extraField = new ExtraField('lp_item');
$extraField->addElements($form, $id);
}
if ($action === 'add') {
$form->addButtonSave(get_lang('AddExercise'), 'submit_button');
} else {
@ -7977,6 +7985,11 @@ class learnpath
}
}
if ('edit' === $action) {
$extraField = new ExtraField('lp_item');
$extraField->addElements($form, $id);
}
if ($action == 'add') {
$form->addButtonSave(get_lang('AddForumToCourse'), 'submit_button');
} else {
@ -8179,6 +8192,11 @@ class learnpath
}
}
if ('edit' === $action) {
$extraField = new ExtraField('lp_item');
$extraField->addElements($form, $id);
}
$form->addButtonSave(get_lang('Ok'), 'submit_button');
if ($action == 'move') {
@ -8703,6 +8721,11 @@ class learnpath
reset($arrLP);
}
if ('edit' === $action) {
$extraField = new ExtraField('lp_item');
$extraField->addElements($form, $id);
}
if ($action !== 'move') {
$arrHide = [];
for ($i = 0; $i < count($arrLP); $i++) {
@ -9050,6 +9073,11 @@ class learnpath
reset($arrLP);
}
if ('edit' === $action) {
$extraField = new ExtraField('lp_item');
$extraField->addElements($form, $id);
}
$arrHide = [];
for ($i = 0; $i < count($arrLP); $i++) {
@ -9425,6 +9453,11 @@ class learnpath
}
}
if ('edit' === $action) {
$extraField = new ExtraField('lp_item');
$extraField->addElements($form, $id);
}
if ($action == 'add') {
$form->addButtonSave(get_lang('AddLinkToCourse'), 'submit_button');
} else {
@ -9581,6 +9614,11 @@ class learnpath
}
}
if ('edit' === $action) {
$extraField = new ExtraField('lp_item');
$extraField->addElements($form, $id);
}
if ($action == 'add') {
$form->addButtonCreate(get_lang('AddAssignmentToCourse'), 'submit_button');
} else {

@ -2802,6 +2802,11 @@ class learnpathItem
if ($debug) {
error_log("type: $type");
}
if (!WhispeakAuthPlugin::isAllowedToSaveLpItem($this->iId)) {
return false;
}
switch ($type) {
case 'asset':
if ($prereqs_complete) {

@ -86,6 +86,14 @@ function switch_item_details($lp_id, $user_id, $view_id, $current_item, $next_it
}
break;
}
if (WhispeakAuthPlugin::isLpItemMarked($new_item_id)) {
ChamiloSession::write(
WhispeakAuthPlugin::SESSION_LP_ITEM,
['lp' => $lp_id, 'lp_item' => $new_item_id, 'src' => '']
);
}
$mylp->start_current_item(true);
if ($mylp->force_commit) {
$mylp->save_current();

@ -59,6 +59,15 @@ if ($dir) {
$src = $learnPath->get_link('http', $lpItemId);
$learnPath->start_current_item(); // starts time counter manually if asset
$src = $learnPath->fixBlockedLinks($src);
if (WhispeakAuthPlugin::isLpItemMarked($lpItemId)) {
ChamiloSession::write(
WhispeakAuthPlugin::SESSION_LP_ITEM,
['lp' => $learnPath->lp_id, 'lp_item' => $lpItemId, 'src' => $src]
);
$src = api_get_path(WEB_PLUGIN_PATH).'whispeakauth/authentify.php';
}
break;
}
$src = 'blank.php?error=prerequisites&prerequisite_message='.Security::remove_XSS($learnPath->error);

@ -885,6 +885,11 @@ switch ($action) {
if (isset($_POST['content_lp'])) {
$_SESSION['oLP']->edit_document($_course);
}
$is_success = true;
$extraFieldValues = new ExtraFieldValue('lp_item');
$extraFieldValues->saveFieldValues($_POST);
Display::addFlash(Display::return_message(get_lang('Updated')));
$url = api_get_self().'?action=add_item&type=step&lp_id='.intval($_SESSION['oLP']->lp_id).'&'.api_get_cidreq();
header('Location: '.$url);

@ -273,6 +273,17 @@ if (!isset($src)) {
}
$src = $lp->fixBlockedLinks($src);
if (WhispeakAuthPlugin::isLpItemMarked($lp_item_id)) {
ChamiloSession::write(
WhispeakAuthPlugin::SESSION_LP_ITEM,
['lp' => $lp->lp_id, 'lp_item' => $lp_item_id, 'src' => $src]
);
$src = api_get_path(WEB_PLUGIN_PATH).'whispeakauth/authentify.php';
break;
}
$lp->start_current_item(); // starts time counter manually if asset
} else {
$src = 'blank.php?error=prerequisites';

@ -13,8 +13,8 @@ require_once __DIR__.'/../inc/global.inc.php';
require_once '../work/work.lib.php';
api_block_anonymous_users();
$htmlHeadXtra[] = '<script type="text/javascript" src="'.api_get_path(WEB_PUBLIC_PATH).'assets/jquery.easy-pie-chart/dist/jquery.easypiechart.js"></script>';
$htmlHeadXtra[] = '<script type="text/javascript" src="'.api_get_path(WEB_PUBLIC_PATH)
.'assets/jquery.easy-pie-chart/dist/jquery.easypiechart.js"></script>';
$export = isset($_GET['export']) ? $_GET['export'] : false;
$sessionId = isset($_GET['id_session']) ? (int) $_GET['id_session'] : 0;
@ -393,10 +393,13 @@ switch ($action) {
$tpl->assign('session_title', $sessionInfo['name']);
$tpl->assign('student', $user_info['complete_name']);
$tpl->assign('table_progress', $table->toHtml());
$tpl->assign('subtitle', sprintf(
get_lang('InSessionXYouHadTheFollowingResults'),
$sessionInfo['name']
));
$tpl->assign(
'subtitle',
sprintf(
get_lang('InSessionXYouHadTheFollowingResults'),
$sessionInfo['name']
)
);
$tpl->assign('table_course', $courseTable);
$content = $tpl->fetch($tpl->get_template('my_space/pdf_export_student.tpl'));
@ -906,18 +909,18 @@ $timezone_user = UserManager::get_extra_user_data_by_field(
'timezone'
);
$use_users_timezone = api_get_setting('use_users_timezone', 'timezones');
if ($timezone_user['timezone'] != null && $use_users_timezone === 'true') {
$timezone = $timezone_user['timezone'];
}
if ($timezone !== null) {
$userInfo['timezone'] = $timezone;
}
if ($timezone_user['timezone'] != null && $use_users_timezone === 'true') {
$timezone = $timezone_user['timezone'];
}
if ($timezone !== null) {
$userInfo['timezone'] = $timezone;
}
if (is_numeric($avg_student_score)) {
$score = $avg_student_score.'%';
} else {
$score = $avg_student_score;
}
if (is_numeric($avg_student_score)) {
$score = $avg_student_score.'%';
} else {
$score = $avg_student_score;
}
$userInfo['student_score'] = (float) $score;
$userInfo['student_progress'] = (float) $avg_student_progress;
@ -1164,11 +1167,14 @@ if (empty($details)) {
$attendances_faults_avg = '0/0 (0%)';
if (!empty($results_faults_avg['total'])) {
if (api_is_drh()) {
$attendances_faults_avg = '<a title="'.get_lang('GoAttendance').'" href="'.api_get_path(WEB_CODE_PATH)
.'attendance/index.php?cidReq='.$courseCodeItem.'&id_session='.$sId.'&student_id='
.$student_id.'">'
.$results_faults_avg['faults'].'/'.$results_faults_avg['total'].' ('
.$results_faults_avg['porcent'].'%)</a>';
$attendances_faults_avg = Display::url(
$results_faults_avg['faults'].'/'.$results_faults_avg['total']
.' ('.$results_faults_avg['porcent'].'%)',
api_get_path(WEB_CODE_PATH)
.'attendance/index.php?cidReq='.$courseCodeItem.'&id_session='.$sId.'&student_id='
.$student_id,
['title' => get_lang('GoAttendance')]
);
} else {
$attendances_faults_avg = $results_faults_avg['faults'].'/'
.$results_faults_avg['total']
@ -1245,13 +1251,13 @@ if (empty($details)) {
$exportCourseList[$sId][] = $csvRow;
echo '<tr>
<td ><a href="'.$courseInfoItem['course_public_url'].'?id_session='.$sId.'">'.
<td><a href="'.$courseInfoItem['course_public_url'].'?id_session='.$sId.'">'.
$courseInfoItem['title'].'</a></td>
<td >'.$time_spent_on_course.'</td>
<td >'.$progress.'</td>
<td >'.$score.'</td>
<td >'.$attendances_faults_avg.'</td>
<td >'.$scoretotal_display.'</td>';
<td>'.$time_spent_on_course.'</td>
<td>'.$progress.'</td>
<td>'.$score.'</td>
<td>'.$attendances_faults_avg.'</td>
<td>'.$scoretotal_display.'</td>';
if (!empty($coachId)) {
echo '<td width="10"><a href="'.api_get_self().'?student='.$user_info['user_id']
@ -1260,7 +1266,8 @@ if (empty($details)) {
.Display::return_icon('2rightarrow.png', get_lang('Details')).'</a></td>';
} else {
echo '<td width="10"><a href="'.api_get_self().'?student='.$user_info['user_id']
.'&details=true&course='.$courseInfoItem['code'].'&origin='.$origin.'&id_session='.$sId.'#infosStudent">'
.'&details=true&course='.$courseInfoItem['code'].'&origin='.$origin.'&id_session='.$sId
.'#infosStudent">'
.Display::return_icon('2rightarrow.png', get_lang('Details')).'</a></td>';
}
echo '</tr>';
@ -1300,29 +1307,32 @@ if (empty($details)) {
$exportCourseList[$sId][] = $csvRow;
$sessionAction = Display::url(
Display::return_icon('export_csv.png', get_lang('ExportAsCSV'), [], ICON_SIZE_MEDIUM),
$currentUrl
.'&'
$currentUrl.'&'
.http_build_query(
['action' => 'export_one_session_row', 'export' => 'csv', 'session_to_export' => $sId]
[
'action' => 'export_one_session_row',
'export' => 'csv',
'session_to_export' => $sId,
]
)
);
$sessionAction .= Display::url(
Display::return_icon('export_excel.png', get_lang('ExportAsXLS'), [], ICON_SIZE_MEDIUM),
$currentUrl
.'&'
$currentUrl.'&'
.http_build_query(
['action' => 'export_one_session_row', 'export' => 'xls', 'session_to_export' => $sId]
[
'action' => 'export_one_session_row',
'export' => 'xls',
'session_to_export' => $sId,
]
)
);
if (!empty($sId)) {
$sessionAction .= Display::url(
Display::return_icon('pdf.png', get_lang('ExportToPDF'), [], ICON_SIZE_MEDIUM),
$currentUrl
.'&'
.http_build_query(
['action' => 'export_to_pdf', 'session_to_export' => $sId]
)
$currentUrl.'&'
.http_build_query(['action' => 'export_to_pdf', 'session_to_export' => $sId])
);
}
echo $sessionAction;
@ -1351,11 +1361,7 @@ if (empty($details)) {
['align' => 'absmiddle', 'hspace' => '3px']
),
'progress' => get_lang('Progress').
Display::return_icon(
'info3.gif',
get_lang('LPProgressScore'),
['align' => 'absmiddle', 'hspace' => '3px']
),
Display::return_icon('info3.gif', get_lang('LPProgressScore'), ['align' => 'absmiddle', 'hspace' => '3px']),
'last_connection' => get_lang('LastConnexion').
Display::return_icon(
'info3.gif',
@ -1393,6 +1399,19 @@ if (empty($details)) {
$columnName
);
}
$hookLpTracking = HookMyStudentsLpTracking::create();
if ($hookLpTracking) {
$hookHeaders = $hookLpTracking->notifyTrackingHeader();
foreach ($hookHeaders as $hookHeader) {
$columnHeadersToExport[] = $hookHeader['value'];
$headers .= Display::tag('th', $hookHeader['value'], $hookHeader['attrs']);
}
}
$csv_content[] = $columnHeadersToExport;
$columnHeadersKeys = array_keys($columnHeaders);
$categoriesTempList = learnpath::getCategories($courseInfo['real_id']);
@ -1588,6 +1607,16 @@ if (empty($details)) {
echo Display::tag('td', $start_time);
}
if ($hookLpTracking) {
$hookContents = $hookLpTracking->notifyTrackingContent($lp_id, $student_id);
foreach ($hookContents as $hookContent) {
$contentToExport[] = strip_tags($hookContent['value']);
echo Display::tag('td', $hookContent['value'], $hookContent['attrs']);
}
}
$csv_content[] = $contentToExport;
if ($any_result === true) {
@ -1622,8 +1651,9 @@ if (empty($details)) {
);
}
echo '</td>';
echo '</tr>';
}
echo '</tr>';
}
echo '</tbody></table></div>';
}
@ -1631,20 +1661,31 @@ if (empty($details)) {
if ($user_info['status'] != INVITEE) {
echo '<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>';
<table class="table table-striped table-hover">
<thead>
<tr>';
echo '<th>'.get_lang('Exercises').'</th>';
echo '<th>'.get_lang('LearningPath').'</th>';
echo '<th>'.get_lang('AvgCourseScore').' '.
Display::return_icon(
'info3.gif',
get_lang('AverageScore'),
['align' => 'absmiddle', 'hspace' => '3px']
).'</th>';
echo '<th>'.get_lang('AvgCourseScore').PHP_EOL
.Display::return_icon('info3.gif', get_lang('AverageScore'), ['align' => 'absmiddle', 'hspace' => '3px'])
.'</th>';
echo '<th>'.get_lang('Attempts').'</th>';
echo '<th>'.get_lang('LatestAttempt').'</th>';
echo '<th>'.get_lang('AllAttempts').'</th>';
$hookQuizTracking = HookMyStudentsQuizTracking::create();
if ($hookQuizTracking) {
$hookHeaders = array_map(
function ($hookHeader) {
return Display::tag('th', $hookHeader['value'], $hookHeader['attrs']);
},
$hookQuizTracking->notifyTrackingHeader()
);
echo implode(PHP_EOL, $hookHeaders);
}
echo '</tr></thead><tbody>';
$csv_content[] = [];
@ -1655,6 +1696,18 @@ if (empty($details)) {
get_lang('Attempts'),
];
if ($hookQuizTracking) {
$hookHeaders = array_map(
function ($hookHeader) {
return strip_tags($hookHeader['value']);
},
$hookQuizTracking->notifyTrackingHeader()
);
$csvContentIndex = count($csv_content) - 1;
$csv_content[$csvContentIndex] = array_merge($csv_content[$csvContentIndex], $hookHeaders);
}
$t_quiz = Database::get_course_table(TABLE_QUIZ_TEST);
$sessionCondition = api_get_session_condition(
$sessionId,
@ -1695,6 +1748,10 @@ if (empty($details)) {
);
$lp_name = '-';
$hookContents = $hookQuizTracking
? $hookQuizTracking->notifyTrackingContent($exercise_id, $student_id)
: [];
if (!isset($score_percentage) && $count_attempts > 0) {
$scores_lp = Tracking::get_avg_student_exercise_score(
$student_id,
@ -1780,6 +1837,13 @@ if (empty($details)) {
);
}
echo '</td>';
if (!empty($hookContents)) {
foreach ($hookContents as $hookContent) {
echo Display::tag('td', $hookContent['value'], $hookContent['attrs']);
}
}
echo '</tr>';
$data_exercices[$i][] = $exercices['title'];
$data_exercices[$i][] = $score_percentage.'%';
@ -1792,6 +1856,14 @@ if (empty($details)) {
$count_attempts,
];
if (!empty($hookContents)) {
$csvContentIndex = count($csv_content) - 1;
foreach ($hookContents as $hookContent) {
$csv_content[$csvContentIndex][] = strip_tags($hookContent['value']);
}
}
$i++;
}
} else {
@ -1802,7 +1874,6 @@ if (empty($details)) {
// @when using sessions we do not show the survey list
if (empty($sessionId)) {
$survey_list = SurveyManager::get_surveys($courseCode, $sessionId);
if (!empty($survey_list)) {
$survey_data = [];
foreach ($survey_list as $survey) {

@ -0,0 +1,345 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\WhispeakAuth\Controller;
use Chamilo\PluginBundle\Entity\WhispeakAuth\LogEvent;
use Chamilo\PluginBundle\WhispeakAuth\Request\ApiRequest;
use Chamilo\UserBundle\Entity\User;
use ChamiloSession;
use Display;
use Login;
use WhispeakAuthPlugin;
/**
* Class AuthenticationController.
*
* @package Chamilo\PluginBundle\WhispeakAuth\Controller
*/
class AuthenticationController extends BaseController
{
/**
* @throws \Exception
*/
public function index()
{
if (!$this->plugin->toolIsEnabled()) {
throw new \Exception(get_lang('NotAllowed'));
}
/** @var array $lpQuestionInfo */
$lpQuestionInfo = ChamiloSession::read(WhispeakAuthPlugin::SESSION_QUIZ_QUESTION, []);
if (ChamiloSession::read(WhispeakAuthPlugin::SESSION_AUTH_PASSWORD, false)) {
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_AUTH_PASSWORD);
if (empty($lpQuestionInfo)) {
$message = $this->plugin->get_lang('MaxAttemptsReached')
.'<br><strong>'.$this->plugin->get_lang('LoginWithUsernameAndPassword').'</strong>';
Display::addFlash(
Display::return_message($message, 'warning')
);
}
header('Location: '.api_get_path(WEB_PLUGIN_PATH).'whispeakauth/authentify_password.php');
exit;
}
/** @var array $lpItemInfo */
$lpItemInfo = ChamiloSession::read(WhispeakAuthPlugin::SESSION_LP_ITEM, []);
/** @var \learnpath $oLp */
$oLp = ChamiloSession::read('oLP', null);
/** @var \Exercise $objExercise */
$objExercise = ChamiloSession::read('objExercise', null);
$isAuthOnLp = !empty($lpItemInfo) && !empty($oLp);
$isAuthOnQuiz = !empty($lpQuestionInfo) && !empty($objExercise);
$showFullPage = !$isAuthOnLp && !$isAuthOnQuiz;
$user = api_get_user_entity(
ChamiloSession::read(WhispeakAuthPlugin::SESSION_2FA_USER, 0) ?: api_get_user_id()
);
$showForm = !$user;
if ($user) {
if (!WhispeakAuthPlugin::getAuthUidValue($user)) {
$message = Display::return_message($this->plugin->get_lang('SpeechAuthNotEnrolled'), 'warning');
if (!empty($lpQuestionInfo) && empty($lpItemInfo)) {
echo $message;
} else {
Display::addFlash($message);
}
header('Location: '.api_get_path(WEB_PLUGIN_PATH).'whispeakauth/authentify_password.php');
exit;
}
}
if (!empty($lpQuestionInfo) && empty($lpItemInfo)) {
echo api_get_js('rtc/RecordRTC.js');
echo api_get_js_simple(api_get_path(WEB_PLUGIN_PATH).'whispeakauth/assets/js/RecordAudio.js');
}
$request = new ApiRequest();
$response = $request->createAuthenticationSessionToken($user);
if (empty($response['text'])) {
$varNumber = mt_rand(1, 6);
$response['text'] = $this->plugin->get_lang("AuthentifySampleText$varNumber");
}
ChamiloSession::write(WhispeakAuthPlugin::SESSION_SENTENCE_TEXT, $response['token']);
if (!empty($lpQuestionInfo) && empty($lpItemInfo)) {
$template = new \Template('', $showFullPage, $showFullPage, false, true, false);
$template->assign('show_form', $showForm);
$template->assign('sample_text', $response['text']);
echo $template->fetch('whispeakauth/view/authentify_recorder.html.twig');
exit;
}
$this->displayPage(
[
'show_form' => $showForm,
'sample_text' => $response['text'],
]
);
}
/**
* @throws \Exception
*/
public function ajax()
{
$userId = api_get_user_id();
$user2fa = ChamiloSession::read(WhispeakAuthPlugin::SESSION_2FA_USER, 0);
if (!empty($user2fa) || !empty($userId)) {
$isAllowed = !empty($_FILES['audio']);
} else {
$isAllowed = !empty($_POST['username']) && !empty($_FILES['audio']);
}
if (!$isAllowed || !$this->plugin->toolIsEnabled()) {
throw new \Exception(get_lang('NotAllowed'));
}
if (!empty($user2fa)) {
$user = api_get_user_entity($user2fa);
} elseif (!empty($userId)) {
$user = api_get_user_entity($userId);
} else {
/** @var User|null $user */
$user = \UserManager::getRepository()->findOneBy(['username' => $_POST['username']]);
}
if (!$user) {
throw new \Exception(get_lang('NotFound'));
}
$audioFilePath = $this->uploadAudioFile($user);
$failedLogins = ChamiloSession::read(WhispeakAuthPlugin::SESSION_FAILED_LOGINS, 0);
$maxAttempts = $this->plugin->getMaxAttempts();
if ($maxAttempts && $failedLogins >= $maxAttempts) {
throw new \Exception($this->plugin->get_lang('MaxAttemptsReached'));
}
$wsid = WhispeakAuthPlugin::getAuthUidValue($user->getId());
if (empty($wsid)) {
throw new \Exception($this->plugin->get_lang('SpeechAuthNotEnrolled'));
}
$token = \ChamiloSession::read(\WhispeakAuthPlugin::SESSION_SENTENCE_TEXT);
$request = new ApiRequest();
$success = $request->performAuthentication($token, $wsid->getValue(), $audioFilePath);
\ChamiloSession::erase(\WhispeakAuthPlugin::SESSION_SENTENCE_TEXT);
/** @var array $lpItemInfo */
$lpItemInfo = ChamiloSession::read(WhispeakAuthPlugin::SESSION_LP_ITEM, []);
/** @var array $quizQuestionInfo */
$quizQuestionInfo = ChamiloSession::read(WhispeakAuthPlugin::SESSION_QUIZ_QUESTION, []);
$message = $this->plugin->get_lang('AuthentifySuccess');
if (!$success) {
if (!empty($lpItemInfo)) {
$this->plugin->addAttemptInLearningPath(
LogEvent::STATUS_FAILED,
$user->getId(),
$lpItemInfo['lp_item'],
$lpItemInfo['lp']
);
}
if (!empty($quizQuestionInfo)) {
$this->plugin->addAttemptInQuiz(
LogEvent::STATUS_FAILED,
$user->getId(),
$quizQuestionInfo['question'],
$quizQuestionInfo['quiz']
);
}
if (empty($lpItemInfo) && empty($quizQuestionInfo)) {
$this->plugin->addAuthenticationAttempt(LogEvent::STATUS_FAILED, $user->getId());
}
$message = $this->plugin->get_lang('AuthentifyFailed');
ChamiloSession::write(WhispeakAuthPlugin::SESSION_FAILED_LOGINS, ++$failedLogins);
if ($maxAttempts && $failedLogins >= $maxAttempts) {
$message .= PHP_EOL
.'<span data-reach-attempts="true">'.$this->plugin->get_lang('MaxAttemptsReached').'</span>'
.PHP_EOL
.'<br><strong>'
.$this->plugin->get_lang('LoginWithUsernameAndPassword')
.'</strong>';
if (!empty($user2fa)) {
Display::addFlash(
Display::return_message($message, 'warning', false)
);
}
} else {
$message .= PHP_EOL.$this->plugin->get_lang('TryAgain');
if ('true' === api_get_setting('allow_lostpassword')) {
$message .= '<br>'
.Display::url(
get_lang('LostPassword'),
api_get_path(WEB_CODE_PATH).'auth/lostPassword.php',
['target' => $lpItemInfo ? '_top' : '_self']
);
}
}
}
echo Display::return_message(
$message,
$success ? 'success' : 'warning',
false
);
if (!$success && $maxAttempts && $failedLogins >= $maxAttempts) {
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_FAILED_LOGINS);
if (!empty($lpItemInfo)) {
echo '<script>window.location.href = "'
.api_get_path(WEB_PLUGIN_PATH)
.'whispeakauth/authentify_password.php";</script>';
exit;
}
if (!empty($quizQuestionInfo)) {
$url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit.php?'.$quizQuestionInfo['url_params'];
ChamiloSession::write(WhispeakAuthPlugin::SESSION_AUTH_PASSWORD, true);
echo "<script>window.location.href = '".$url."';</script>";
exit;
}
echo '<script>window.location.href = "'.api_get_path(WEB_PATH).'";</script>';
exit;
}
if ($success) {
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_SENTENCE_TEXT);
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_FAILED_LOGINS);
if (!empty($lpItemInfo)) {
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_LP_ITEM);
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_2FA_USER);
$this->plugin->addAttemptInLearningPath(
LogEvent::STATUS_SUCCESS,
$user->getId(),
$lpItemInfo['lp_item'],
$lpItemInfo['lp']
);
echo '<script>window.location.href = "'.$lpItemInfo['src'].'";</script>';
exit;
}
if (!empty($quizQuestionInfo)) {
$quizQuestionInfo['passed'] = true;
$url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit.php?'.$quizQuestionInfo['url_params'];
ChamiloSession::write(WhispeakAuthPlugin::SESSION_QUIZ_QUESTION, $quizQuestionInfo);
$this->plugin->addAttemptInQuiz(
LogEvent::STATUS_SUCCESS,
$user->getId(),
$quizQuestionInfo['question'],
$quizQuestionInfo['quiz']
);
echo '<script>window.location.href = "'.$url.'";</script>';
exit;
}
if (empty($lpItemInfo) && empty($quizQuestionInfo)) {
$this->plugin->addAuthenticationAttempt(LogEvent::STATUS_SUCCESS, $user->getId());
}
$loggedUser = [
'user_id' => $user->getId(),
'status' => $user->getStatus(),
'uidReset' => true,
];
if (empty($user2fa)) {
ChamiloSession::write(WhispeakAuthPlugin::SESSION_2FA_USER, $user->getId());
}
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_FAILED_LOGINS);
ChamiloSession::write('_user', $loggedUser);
Login::init_user($user->getId(), true);
echo '<script>window.location.href = "'.api_get_path(WEB_PATH).'";</script>';
}
}
/**
* @inheritDoc
*/
protected function displayPage(array $variables)
{
global $htmlHeadXtra;
$htmlHeadXtra[] = api_get_js('rtc/RecordRTC.js');
$htmlHeadXtra[] = api_get_js_simple(api_get_path(WEB_PLUGIN_PATH).'whispeakauth/assets/js/RecordAudio.js');
$pageTitle = $this->plugin->get_title();
$template = new \Template($pageTitle);
foreach ($variables as $key => $value) {
$template->assign($key, $value);
}
$pageContent = $template->fetch('whispeakauth/view/authentify_recorder.html.twig');
$template->assign('header', $pageTitle);
$template->assign('content', $pageContent);
$template->display_one_col_template();
}
}

@ -0,0 +1,66 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\WhispeakAuth\Controller;
use Chamilo\UserBundle\Entity\User;
use FFMpeg\FFMpeg;
use FFMpeg\Format\Audio\Wav;
/**
* Class BaseController.
*
* @package Chamilo\PluginBundle\WhispeakAuth\Controller
*/
abstract class BaseController
{
/**
* @var \WhispeakAuthPlugin
*/
protected $plugin;
/**
* BaseController constructor.
*/
public function __construct()
{
$this->plugin = \WhispeakAuthPlugin::create();
}
/**
* @param array $variables
*/
abstract protected function displayPage(array $variables);
/**
* @param \Chamilo\UserBundle\Entity\User $user
*
* @throws \Exception
* @return string
*/
protected function uploadAudioFile(User $user)
{
$pluginName = $this->plugin->get_name();
$path = api_upload_file($pluginName, $_FILES['audio'], $user->getId());
if (false === $path) {
throw new \Exception(get_lang('UploadError'));
}
$fullPath = api_get_path(SYS_UPLOAD_PATH).$pluginName.$path['path_to_save'];
$mimeType = mime_content_type($fullPath);
if ('wav' !== substr($mimeType, -3)) {
$ffmpeg = FFMpeg::create();
$audioFile = $ffmpeg->open($fullPath);
$fullPath = dirname($fullPath).'/audio.wav';
$audioFile->save(new Wav(), $fullPath);
}
return $fullPath;
}
}

@ -0,0 +1,98 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\WhispeakAuth\Controller;
use Chamilo\PluginBundle\WhispeakAuth\Request\ApiRequest;
/**
* Class EnrollmentController.
*
* @package Chamilo\PluginBundle\WhispeakAuth\Controller
*/
class EnrollmentController extends BaseController
{
/**
* @throws \Exception
*/
public function index()
{
if (!$this->plugin->toolIsEnabled()) {
throw new \Exception(get_lang('NotAllowed'));
}
$user = api_get_user_entity(api_get_user_id());
$userIsEnrolled = \WhispeakAuthPlugin::checkUserIsEnrolled($user->getId());
if ($userIsEnrolled) {
throw new \Exception($this->plugin->get_lang('SpeechAuthAlreadyEnrolled'));
}
$request = new ApiRequest();
$response = $request->createEnrollmentSessionToken($user);
\ChamiloSession::write(\WhispeakAuthPlugin::SESSION_SENTENCE_TEXT, $response['token']);
$this->displayPage(
[
'action' => 'enrollment',
'sample_text' => $response['text'],
]
);
}
/**
* @throws \Exception
*/
public function ajax()
{
if (!$this->plugin->toolIsEnabled() || empty($_FILES['audio'])) {
throw new \Exception(get_lang('NotAllowed'));
}
$user = api_get_user_entity(api_get_user_id());
$audioFilePath = $this->uploadAudioFile($user);
$token = \ChamiloSession::read(\WhispeakAuthPlugin::SESSION_SENTENCE_TEXT);
if (empty($token)) {
throw new \Exception($this->plugin->get_lang('EnrollmentFailed'));
}
$request = new ApiRequest();
$response = $request->createEnrollment($token, $audioFilePath);
\ChamiloSession::erase(\WhispeakAuthPlugin::SESSION_SENTENCE_TEXT);
$this->plugin->saveEnrollment($user, $response['speaker']);
echo \Display::return_message($this->plugin->get_lang('EnrollmentSuccess'), 'success');
}
/**
* @inheritDoc
*/
protected function displayPage(array $variables)
{
global $htmlHeadXtra;
$htmlHeadXtra[] = api_get_js('rtc/RecordRTC.js');
$htmlHeadXtra[] = api_get_js_simple(api_get_path(WEB_PLUGIN_PATH).'whispeakauth/assets/js/RecordAudio.js');
$pageTitle = $this->plugin->get_title();
$template = new \Template($pageTitle);
foreach ($variables as $key => $value) {
$template->assign($key, $value);
}
$pageContent = $template->fetch('whispeakauth/view/record_audio.html.twig');
$template->assign('header', $pageTitle);
$template->assign('content', $pageContent);
$template->display_one_col_template();
}
}

@ -0,0 +1,145 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\WhispeakAuth;
use Chamilo\UserBundle\Entity\User;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
/**
* Class LogEvent.
*
* @package Chamilo\PluginBundle\Entity\WhispeakAuth
*
* @ORM\Table(name="whispeak_log_event")
* @ORM\Entity()
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="discr", type="string")
* @ORM\DiscriminatorMap({
* "log_event" = "Chamilo\PluginBundle\Entity\WhispeakAuth\LogEvent",
* "log_event_lp" = "Chamilo\PluginBundle\Entity\WhispeakAuth\LogEventLp",
* "log_event_quiz" = "Chamilo\PluginBundle\Entity\WhispeakAuth\LogEventQuiz"
* })
*/
class LogEvent
{
const STATUS_FAILED = 0;
const STATUS_SUCCESS = 1;
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id()
* @ORM\GeneratedValue()
*/
private $id;
/**
* @var DateTime
*
* @ORM\Column(name="datetime", type="datetime")
*/
private $datetime;
/**
* @var int
*
* @ORM\Column(name="action_status", type="smallint")
*/
private $actionStatus;
/**
* @var User
*
* @ORM\ManyToOne(targetEntity="Chamilo\UserBundle\Entity\User")
* @ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=false)
*/
private $user;
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @param int $id
*
* @return LogEvent
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* @return DateTime
*/
public function getDatetime()
{
return $this->datetime;
}
/**
* @param DateTime $datetime
*
* @return LogEvent
*/
public function setDatetime($datetime)
{
$this->datetime = $datetime;
return $this;
}
/**
* @return int
*/
public function getActionStatus()
{
return $this->actionStatus;
}
/**
* @param int $actionStatus
*
* @return LogEvent
*/
public function setActionStatus($actionStatus)
{
$this->actionStatus = $actionStatus;
return $this;
}
/**
* @return User
*/
public function getUser()
{
return $this->user;
}
/**
* @param User $user
*
* @return LogEvent
*/
public function setUser($user)
{
$this->user = $user;
return $this;
}
/**
* @return string
*/
public function getTypeString()
{
return '-';
}
}

@ -0,0 +1,84 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\WhispeakAuth;
use Chamilo\CourseBundle\Entity\CLp;
use Chamilo\CourseBundle\Entity\CLpItem;
use Doctrine\ORM\Mapping as ORM;
/**
* Class LogEventLp.
*
* @package Chamilo\PluginBundle\Entity\WhispeakAuth
*
* @ORM\Entity()
*/
class LogEventLp extends LogEvent
{
/**
* @var CLpItem
*
* @ORM\ManyToOne(targetEntity="Chamilo\CourseBundle\Entity\CLpItem")
* @ORM\JoinColumn(name="lp_item_id", referencedColumnName="iid")
*/
private $lpItem;
/**
* @var CLp
*
* @ORM\ManyToOne(targetEntity="Chamilo\CourseBundle\Entity\CLp")
* @ORM\JoinColumn(name="lp_id", referencedColumnName="iid")
*/
private $lp;
/**
* @return CLpItem
*/
public function getLpItem()
{
return $this->lpItem;
}
/**
* @param CLpItem $lpItem
*
* @return LogEventLp
*/
public function setLpItem($lpItem)
{
$this->lpItem = $lpItem;
return $this;
}
/**
* @return CLp
*/
public function getLp()
{
return $this->lp;
}
/**
* @param CLp $lp
*
* @return LogEventLp
*/
public function setLp($lp)
{
$this->lp = $lp;
return $this;
}
/**
* @inheritDoc
*/
public function getTypeString()
{
$lpName = $this->lp->getName();
$itemTitle = $this->getLpItem()->getTitle();
return "$lpName > $itemTitle";
}
}

@ -0,0 +1,84 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\WhispeakAuth;
use Chamilo\CourseBundle\Entity\CQuiz;
use Chamilo\CourseBundle\Entity\CQuizQuestion;
use Doctrine\ORM\Mapping as ORM;
/**
* Class LogEventQuiz.
*
* @package Chamilo\PluginBundle\Entity\WhispeakAuth
*
* @ORM\Entity()
*/
class LogEventQuiz extends LogEvent
{
/**
* @var CQuizQuestion
*
* @ORM\ManyToOne(targetEntity="Chamilo\CourseBundle\Entity\CQuizQuestion")
* @ORM\JoinColumn(name="question_id", referencedColumnName="iid")
*/
private $question;
/**
* @var CQuiz
*
* @ORM\ManyToOne(targetEntity="Chamilo\CourseBundle\Entity\CQuiz")
* @ORM\JoinColumn(name="quiz_id", referencedColumnName="iid")
*/
private $quiz;
/**
* @return CQuizQuestion
*/
public function getQuestion()
{
return $this->question;
}
/**
* @param CQuizQuestion $question
*
* @return LogEventQuiz
*/
public function setQuestion($question)
{
$this->question = $question;
return $this;
}
/**
* @return CQuiz
*/
public function getQuiz()
{
return $this->quiz;
}
/**
* @param CQuiz $quiz
*
* @return LogEventQuiz
*/
public function setQuiz($quiz)
{
$this->quiz = $quiz;
return $this;
}
/**
* @inheritDoc
*/
public function getTypeString()
{
$quiz = strip_tags($this->getQuiz()->getTitle());
$question = strip_tags($this->getQuestion()->getQuestion());
return "$quiz > $question";
}
}

@ -1,10 +1,20 @@
Speech authentication with Whispeak
===================================
**Notice:**
This plugin requires the user to grant permission to use the microphone connected on the web browser. Currently,
browsers are limiting this permission to be used only in a secure environment with HTTPS.
**If your portal does not work with HTTP, then Whispeak authentication may not work.**
Instructions:
-------------
> Make sure the directory `src/Chamilo\PluginBundle` is writable by the web server in order for the plugin is installed
> properly. This might imply a manual change on your server (outside of the Chamilo interface).
1. Install plugin in Chamilo.
2. Set the plugin configuration with the token and API url. And enable the plugin.
2. Set the plugin configuration enabling the plugin and (optionally) set the max attempts.
3. Set the `login_bottom` region to the plugin.
4. Add `$_configuration['whispeak_auth_enabled'] = true;` to `configuration.php` file.
5. Optionally, you can add the `menu_administrator` region to se the user logged activities from Whispeak.

@ -0,0 +1,188 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\WhispeakAuth\Request;
use Chamilo\UserBundle\Entity\User;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
/**
* Class ApiRequest.
*
* @package Chamilo\PluginBundle\WhispeakAuth\Request
*/
class ApiRequest
{
/**
* @var \WhispeakAuthPlugin
*/
protected $plugin;
/**
* @var string
*/
protected $apiKey;
/**
* BaseController constructor.
*/
public function __construct()
{
$this->plugin = \WhispeakAuthPlugin::create();
$this->apiKey = $this->plugin->get(\WhispeakAuthPlugin::SETTING_TOKEN);
}
/**
* Create a session token to perform an enrollment.
*
* @param \Chamilo\UserBundle\Entity\User $user
*
* @throws \Exception
*
* @return array
*/
public function createEnrollmentSessionToken(User $user)
{
$apiKey = $this->plugin->get(\WhispeakAuthPlugin::SETTING_TOKEN);
$langIso = api_get_language_isocode($user->getLanguage());
return $this->sendRequest(
'get',
'enroll',
$apiKey,
['lang' => $langIso]
);
}
/**
* @param string $token
* @param string $audioFilePath
*
* @throws \Exception
*
* @return array
*/
public function createEnrollment($token, $audioFilePath)
{
return $this->sendRequest(
'post',
'enroll',
$token,
[],
[
[
'name' => 'file',
'contents' => fopen($audioFilePath, 'r'),
'filename' => basename($audioFilePath),
],
]
);
}
/**
* @param \Chamilo\UserBundle\Entity\User|null $user
*
* @throws \Exception
*
* @return array
*/
public function createAuthenticationSessionToken(User $user = null)
{
$apiKey = $this->plugin->get(\WhispeakAuthPlugin::SETTING_TOKEN);
$langIso = api_get_language_isocode($user ? $user->getLanguage() : null);
return $this->sendRequest(
'get',
'auth',
$apiKey,
['lang' => $langIso]
);
}
/**
* @param string $token
* @param string $speaker
* @param string $audioFilePath
*
* @throws \Exception
*
* @return bool
*/
public function performAuthentication($token, $speaker, $audioFilePath)
{
try {
$this->sendRequest(
'post',
'auth',
$token,
[],
[
[
'name' => 'speaker',
'contents' => $speaker,
],
[
'name' => 'file',
'contents' => fopen($audioFilePath, 'r'),
'filename' => basename($audioFilePath),
],
]
);
return true;
} catch (\Exception $e) {
return false;
}
}
/**
* @param string $method
* @param string $uri
* @param string $authBearer
* @param array $query
* @param array $multipart
*
* @throws \Exception
*
* @return array
*/
private function sendRequest($method, $uri, $authBearer, array $query = [], array $multipart = [])
{
$httpClient = new Client(['base_uri' => $this->plugin->getApiUrl()]);
try {
$responseBody = $httpClient
->request(
$method,
$uri,
[
'headers' => ['Authorization' => "Bearer $authBearer"],
'query' => $query,
'multipart' => $multipart,
]
)
->getBody()
->getContents();
return json_decode($responseBody, true);
} catch (RequestException $requestException) {
if (!$requestException->hasResponse()) {
throw new \Exception($requestException->getMessage());
}
$responseBody = $requestException->getResponse()->getBody()->getContents();
$json = json_decode($responseBody, true);
if (empty($json['message'])) {
throw new \Exception($requestException->getMessage());
}
$message = is_array($json['message']) ? implode(PHP_EOL, $json['message']) : $json['message'];
throw new \Exception($message);
} catch (Exception $exception) {
throw new \Exception($exception->getMessage());
}
}
}

@ -3,19 +3,33 @@
use Chamilo\CoreBundle\Entity\ExtraField;
use Chamilo\CoreBundle\Entity\ExtraFieldValues;
use Chamilo\PluginBundle\Entity\WhispeakAuth\LogEvent;
use Chamilo\PluginBundle\Entity\WhispeakAuth\LogEventLp;
use Chamilo\PluginBundle\Entity\WhispeakAuth\LogEventQuiz;
use Chamilo\UserBundle\Entity\User;
use Symfony\Component\Filesystem\Filesystem;
/**
* Class WhispeakAuthPlugin.
*/
class WhispeakAuthPlugin extends Plugin
class WhispeakAuthPlugin extends Plugin implements HookPluginInterface
{
const SETTING_ENABLE = 'enable';
const SETTING_MAX_ATTEMPTS = 'max_attempts';
const SETTING_2FA = '2fa';
const SETTING_API_URL = 'api_url';
const SETTING_TOKEN = 'token';
const SETTING_INSTRUCTION = 'instruction';
const EXTRAFIELD_AUTH_UID = 'whispeak_auth_uid';
const EXTRAFIELD_LP_ITEM = 'whispeak_lp_item';
const EXTRAFIELD_QUIZ_QUESTION = 'whispeak_quiz_question';
const SESSION_FAILED_LOGINS = 'whispeak_failed_logins';
const SESSION_2FA_USER = 'whispeak_user_id';
const SESSION_LP_ITEM = 'whispeak_lp_item';
const SESSION_QUIZ_QUESTION = 'whispeak_quiz_question';
const SESSION_AUTH_PASSWORD = 'whispeak_auth_password';
const SESSION_SENTENCE_TEXT = 'whispeak_sentence_text';
/**
* StudentFollowUpPlugin constructor.
@ -29,11 +43,24 @@ class WhispeakAuthPlugin extends Plugin
self::SETTING_ENABLE => 'boolean',
self::SETTING_API_URL => 'text',
self::SETTING_TOKEN => 'text',
self::SETTING_INSTRUCTION => 'html',
self::SETTING_MAX_ATTEMPTS => 'text',
self::SETTING_2FA => 'boolean',
]
);
}
/**
* Get the admin URL for the plugin if Plugin::isAdminPlugin is true.
*
* @return string
*/
public function getAdminUrl()
{
$webPath = api_get_path(WEB_PLUGIN_PATH).$this->get_name();
return "$webPath/admin.php";
}
/**
* @return WhispeakAuthPlugin
*/
@ -46,29 +73,24 @@ class WhispeakAuthPlugin extends Plugin
public function install()
{
UserManager::create_extra_field(
self::EXTRAFIELD_AUTH_UID,
\ExtraField::FIELD_TYPE_TEXT,
$this->get_lang('Whispeak uid'),
''
);
$this->installExtraFields();
$this->installEntities();
$this->installHook();
}
public function uninstall()
{
$extraField = self::getAuthUidExtraField();
if (empty($extraField)) {
return;
}
$em = Database::getManager();
$em->createQuery('DELETE FROM ChamiloCoreBundle:ExtraFieldValues efv WHERE efv.field = :field')
->execute(['field' => $extraField]);
$this->uninstallHook();
$this->uninstallExtraFields();
$this->uninstallEntities();
}
$em->remove($extraField);
$em->flush();
/**
* @return string
*/
public function getEntityPath()
{
return api_get_path(SYS_PATH).'src/Chamilo/PluginBundle/Entity/'.$this->getCamelCaseName();
}
/**
@ -90,6 +112,42 @@ class WhispeakAuthPlugin extends Plugin
return $extraField;
}
/**
* @return ExtraField
*/
public static function getLpItemExtraField()
{
$efRepo = Database::getManager()->getRepository('ChamiloCoreBundle:ExtraField');
/** @var ExtraField $extraField */
$extraField = $efRepo->findOneBy(
[
'variable' => self::EXTRAFIELD_LP_ITEM,
'extraFieldType' => ExtraField::LP_ITEM_FIELD_TYPE,
]
);
return $extraField;
}
/**
* @return ExtraField
*/
public static function getQuizQuestionExtraField()
{
$efRepo = Database::getManager()->getRepository('ChamiloCoreBundle:ExtraField');
/** @var ExtraField $extraField */
$extraField = $efRepo->findOneBy(
[
'variable' => self::EXTRAFIELD_QUIZ_QUESTION,
'extraFieldType' => ExtraField::QUESTION_FIELD_TYPE,
]
);
return $extraField;
}
/**
* @param int $userId
*
@ -107,6 +165,98 @@ class WhispeakAuthPlugin extends Plugin
return $value;
}
/**
* Get the whispeak_lp_item value for a LP item ID.
*
* @param int $lpItemId
*
* @return array|false
*/
public static function getLpItemValue($lpItemId)
{
$efv = new ExtraFieldValue('lp_item');
$value = $efv->get_values_by_handler_and_field_variable($lpItemId, self::EXTRAFIELD_LP_ITEM);
return $value;
}
/**
* @param int $lpItemId
*
* @return bool
*/
public static function isLpItemMarked($lpItemId)
{
if (!self::create()->isEnabled()) {
return false;
}
$value = self::getLpItemValue($lpItemId);
return !empty($value) && !empty($value['value']);
}
/**
* Get the whispeak_quiz_question value for a quiz question ID.
*
* @param int $questionId
*
* @return array|false
*/
public static function getQuizQuestionValue($questionId)
{
$efv = new ExtraFieldValue('question');
$value = $efv->get_values_by_handler_and_field_variable($questionId, self::EXTRAFIELD_QUIZ_QUESTION);
return $value;
}
/**
* @param int $questionId
*
* @return bool
*/
public static function isQuizQuestionMarked($questionId)
{
if (!self::create()->isEnabled()) {
return false;
}
$value = self::getQuizQuestionValue($questionId);
return !empty($value) && !empty($value['value']);
}
/**
* @param int $questionId
*
* @return bool
*/
public static function questionRequireAuthentify($questionId)
{
$isMarked = self::isQuizQuestionMarked($questionId);
if (!$isMarked) {
return false;
}
$questionInfo = ChamiloSession::read(self::SESSION_QUIZ_QUESTION, []);
if (empty($questionInfo)) {
return true;
}
if ((int) $questionId !== $questionInfo['question']) {
return true;
}
if (false === $questionInfo['passed']) {
return true;
}
return false;
}
/**
* @param int $userId
*
@ -132,27 +282,7 @@ class WhispeakAuthPlugin extends Plugin
}
/**
* @param string $filePath
*
* @return array
*/
public function requestEnrollment(User $user, $filePath)
{
$metadata = [
'motherTongue' => $user->getLanguage(),
'spokenTongue' => $user->getLanguage(),
'audioType' => 'pcm',
];
return $this->sendRequest(
'enrollment',
$metadata,
$user,
$filePath
);
}
/**
* @param User $user
* @param string $uid
*
* @throws \Doctrine\ORM\OptimisticLockException
@ -160,130 +290,557 @@ class WhispeakAuthPlugin extends Plugin
public function saveEnrollment(User $user, $uid)
{
$em = Database::getManager();
$value = self::getAuthUidValue($user->getId());
$extraFieldValue = self::getAuthUidValue($user->getId());
if (empty($value)) {
$ef = self::getAuthUidExtraField();
if (empty($extraFieldValue)) {
$extraField = self::getAuthUidExtraField();
$now = new DateTime('now', new DateTimeZone('UTC'));
$value = new ExtraFieldValues();
$value
->setField($ef)
$extraFieldValue = new ExtraFieldValues();
$extraFieldValue
->setField($extraField)
->setItemId($user->getId())
->setUpdatedAt($now);
}
$value->setValue($uid);
$extraFieldValue->setValue($uid);
$em->persist($value);
$em->persist($extraFieldValue);
$em->flush();
}
public function requestAuthentify(User $user, $filePath)
/**
* @return bool
*/
public function toolIsEnabled()
{
$value = self::getAuthUidValue($user->getId());
return 'true' === $this->get(self::SETTING_ENABLE);
}
if (empty($value)) {
return null;
/**
* Access not allowed when tool is not enabled.
*
* @param bool $printHeaders Optional. Print headers.
*/
public function protectTool($printHeaders = true)
{
if ($this->toolIsEnabled()) {
return;
}
$metadata = [
'uid' => $value->getValue(),
'audioType' => 'pcm',
];
api_not_allowed($printHeaders);
}
return $this->sendRequest(
'authentify',
$metadata,
$user,
$filePath
);
/**
* Get the max_attemtps option.
*
* @return int
*/
public function getMaxAttempts()
{
return (int) $this->get(self::SETTING_MAX_ATTEMPTS);
}
/**
* @return string
* Install hook when saving the plugin configuration.
*
* @return WhispeakAuthPlugin
*/
public function getAuthentifySampleText()
public function performActionsAfterConfigure()
{
$phrases = [];
$observer = WhispeakConditionalLoginHook::create();
for ($i = 1; $i <= 6; $i++) {
$phrases[] = $this->get_lang("AuthentifySampleText$i");
if ('true' === $this->get(self::SETTING_2FA)) {
HookConditionalLogin::create()->attach($observer);
} else {
HookConditionalLogin::create()->detach($observer);
}
$rand = array_rand($phrases, 1);
return $this;
}
/**
* This method will call the Hook management insertHook to add Hook observer from this plugin.
*/
public function installHook()
{
$observer = WhispeakMyStudentsLpTrackingHook::create();
HookMyStudentsLpTracking::create()->attach($observer);
$observer = WhispeakMyStudentsQuizTrackingHook::create();
HookMyStudentsQuizTracking::create()->attach($observer);
}
return $phrases[$rand];
/**
* This method will call the Hook management deleteHook to disable Hook observer from this plugin.
*/
public function uninstallHook()
{
$observer = WhispeakConditionalLoginHook::create();
HookConditionalLogin::create()->detach($observer);
$observer = WhispeakMyStudentsLpTrackingHook::create();
HookMyStudentsLpTracking::create()->detach($observer);
}
/**
* @param int $userId
*
* @throws \Doctrine\ORM\OptimisticLockException
*
* @return bool
*/
public function toolIsEnabled()
public static function deleteEnrollment($userId)
{
return 'true' === $this->get(self::SETTING_ENABLE);
$extraFieldValue = self::getAuthUidValue($userId);
if (empty($extraFieldValue)) {
return false;
}
$em = Database::getManager();
$em->remove($extraFieldValue);
$em->flush();
return true;
}
/**
* Access not allowed when tool is not enabled.
* Check if the WhispeakAuth plugin is installed and enabled.
*
* @param bool $printHeaders Optional. Print headers.
* @return bool
*/
public function protectTool($printHeaders = true)
public function isEnabled()
{
if ($this->toolIsEnabled()) {
return;
return parent::isEnabled() && 'true' === api_get_plugin_setting('whispeakauth', self::SETTING_ENABLE);
}
/**
* @param int $lpItemId
*
* @return bool
*/
public static function isAllowedToSaveLpItem($lpItemId)
{
if (!self::isLpItemMarked($lpItemId)) {
return true;
}
api_not_allowed($printHeaders);
$markedItem = ChamiloSession::read(self::SESSION_LP_ITEM, []);
if (empty($markedItem)) {
return true;
}
if ((int) $lpItemId !== (int) $markedItem['lp_item']) {
return true;
}
return false;
}
/**
* Display a error message.
*
* @param string|null $error Optional. The message text
*/
public static function displayNotAllowedMessage($error = null)
{
$error = empty($error) ? get_lang('NotAllowed') : $error;
echo Display::return_message($error, 'error', false);
exit;
}
/**
* @param int $questionId
* @param Exercise $exercise
*
* @throws Exception
*
* @return string
*/
private function getApiUrl()
public static function quizQuestionAuthentify($questionId, Exercise $exercise)
{
$url = $this->get(self::SETTING_API_URL);
ChamiloSession::write(
self::SESSION_QUIZ_QUESTION,
[
'quiz' => (int) $exercise->iId,
'question' => (int) $questionId,
'url_params' => $_SERVER['QUERY_STRING'],
'passed' => false,
]
);
$template = new Template('', false, false, false, true, false, false);
$template->assign('question', $questionId);
$template->assign('exercise', $exercise->iId);
$content = $template->fetch('whispeakauth/view/quiz_question.html.twig');
return trim($url, " \t\n\r \v/");
echo $content;
}
/**
* @param string $endPoint
* @param string $filePath
* @param int $status
* @param int $userId
* @param int $lpItemId
* @param int $lpId
*
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\TransactionRequiredException
*
* @return array
* @return LogEventLp|null
*/
private function sendRequest($endPoint, array $metadata, User $user, $filePath)
public function addAttemptInLearningPath($status, $userId, $lpItemId, $lpId)
{
$moderator = $user->getCreatorId() ?: $user->getId();
$apiUrl = $this->getApiUrl()."/$endPoint";
$headers = [
//"Content-Type: application/x-www-form-urlencoded",
"Authorization: Bearer ".$this->get(self::SETTING_TOKEN),
];
$post = [
'metadata' => json_encode($metadata),
'moderator' => "moderator_$moderator",
'client' => base64_encode($user->getUserId()),
'voice' => new CURLFile($filePath),
];
$em = Database::getManager();
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close($ch);
$user = api_get_user_entity($userId);
$lpItem = $em->find('ChamiloCourseBundle:CLpItem', $lpItemId);
$lp = $em->find('ChamiloCourseBundle:CLp', $lpId);
$result = json_decode($result, true);
if (empty($lp) || empty($lpItem)) {
return null;
}
if (!empty($result['error'])) {
$logEvent = new LogEventLp();
$logEvent
->setLpItem($lpItem)
->setLp($lp)
->setUser($user)
->setDatetime(
api_get_utc_datetime(null, false, true)
)
->setActionStatus($status);
$em->persist($logEvent);
$em->flush();
return $logEvent;
}
/**
* @param int $status
* @param int $userId
* @param int $questionId
* @param int $quizId
*
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\TransactionRequiredException
*
* @return LogEventQuiz|null
*/
public function addAttemptInQuiz($status, $userId, $questionId, $quizId)
{
$em = Database::getManager();
$user = api_get_user_entity($userId);
$question = $em->find('ChamiloCourseBundle:CQuizQuestion', $questionId);
$quiz = $em->find('ChamiloCourseBundle:CQuiz', $quizId);
if (empty($quiz) || empty($question)) {
return null;
}
return json_decode($result, true);
$logEvent = new LogEventQuiz();
$logEvent
->setQuestion($question)
->setQuiz($quiz)
->setUser($user)
->setDatetime(
api_get_utc_datetime(null, false, true)
)
->setActionStatus($status);
$em->persist($logEvent);
$em->flush();
return $logEvent;
}
/**
* @param int $status
* @param int $userId
*
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\TransactionRequiredException
*
* @return LogEvent|null
*/
public function addAuthenticationAttempt($status, $userId)
{
$em = Database::getManager();
$user = api_get_user_entity($userId);
$logEvent = new LogEvent();
$logEvent
->setUser($user)
->setDatetime(
api_get_utc_datetime(null, false, true)
)
->setActionStatus($status);
$em->persist($logEvent);
$em->flush();
return $logEvent;
}
/**
* @param int $lpId
* @param int $userId
*
* @throws \Doctrine\ORM\Query\QueryException
*
* @return string
*/
public static function countAllAttemptsInLearningPath($lpId, $userId)
{
$query = Database::getManager()
->createQuery(
'SELECT COUNT(log) AS c FROM ChamiloPluginBundle:WhispeakAuth\LogEventLp log
WHERE log.lp = :lp AND log.user = :user'
)
->setParameters(['lp' => $lpId, 'user' => $userId]);
$totalCount = (int) $query->getSingleScalarResult();
return $totalCount;
}
/**
* @param int $lpId
* @param int $userId
*
* @throws \Doctrine\ORM\Query\QueryException
*
* @return string
*/
public static function countSuccessAttemptsInLearningPath($lpId, $userId)
{
$query = Database::getManager()
->createQuery(
'SELECT COUNT(log) AS c FROM ChamiloPluginBundle:WhispeakAuth\LogEventLp log
WHERE log.lp = :lp AND log.user = :user AND log.actionStatus = :status'
)
->setParameters(['lp' => $lpId, 'user' => $userId, 'status' => LogEvent::STATUS_SUCCESS]);
$totalCount = (int) $query->getSingleScalarResult();
return $totalCount;
}
/**
* @param int $quizId
* @param int $userId
*
* @throws \Doctrine\ORM\Query\QueryException
*
* @return string
*/
public static function countAllAttemptsInQuiz($quizId, $userId)
{
$query = Database::getManager()
->createQuery(
'SELECT COUNT(log) AS c FROM ChamiloPluginBundle:WhispeakAuth\LogEventQuiz log
WHERE log.quiz = :quiz AND log.user = :user'
)
->setParameters(['quiz' => $quizId, 'user' => $userId]);
$totalCount = (int) $query->getSingleScalarResult();
return $totalCount;
}
/**
* @param int $quizId
* @param int $userId
*
* @throws \Doctrine\ORM\Query\QueryException
*
* @return string
*/
public static function countSuccessAttemptsInQuiz($quizId, $userId)
{
$query = Database::getManager()
->createQuery(
'SELECT COUNT(log) AS c FROM ChamiloPluginBundle:WhispeakAuth\LogEventQuiz log
WHERE log.quiz = :quiz AND log.user = :user AND log.actionStatus = :status'
)
->setParameters(['quiz' => $quizId, 'user' => $userId, 'status' => LogEvent::STATUS_SUCCESS]);
$totalCount = (int) $query->getSingleScalarResult();
return $totalCount;
}
/**
* Install extra fields for user, learning path and quiz question.
*/
private function installExtraFields()
{
UserManager::create_extra_field(
self::EXTRAFIELD_AUTH_UID,
\ExtraField::FIELD_TYPE_TEXT,
$this->get_lang('Whispeak uid'),
''
);
LpItem::createExtraField(
self::EXTRAFIELD_LP_ITEM,
\ExtraField::FIELD_TYPE_CHECKBOX,
$this->get_lang('MarkForSpeechAuthentication'),
'0',
true,
true
);
$extraField = new \ExtraField('question');
$params = [
'variable' => self::EXTRAFIELD_QUIZ_QUESTION,
'field_type' => \ExtraField::FIELD_TYPE_CHECKBOX,
'display_text' => $this->get_lang('MarkForSpeechAuthentication'),
'default_value' => '0',
'changeable' => true,
'visible_to_self' => true,
'visible_to_others' => false,
];
$extraField->save($params);
}
/**
* Install the Doctrine's entities.
*/
private function installEntities()
{
$pluginEntityPath = $this->getEntityPath();
if (!is_dir($pluginEntityPath)) {
if (!is_writable(dirname($pluginEntityPath))) {
Display::addFlash(
Display::return_message(get_lang('ErrorCreatingDir').": $pluginEntityPath", 'error')
);
return;
}
mkdir($pluginEntityPath, api_get_permissions_for_new_directories());
}
$fs = new Filesystem();
$fs->mirror(__DIR__.'/Entity/', $pluginEntityPath, null, ['override']);
$schema = Database::getManager()->getConnection()->getSchemaManager();
if (false === $schema->tablesExist('whispeak_log_event')) {
$sql = "CREATE TABLE whispeak_log_event (
id INT AUTO_INCREMENT NOT NULL,
user_id INT NOT NULL,
lp_item_id INT DEFAULT NULL,
lp_id INT DEFAULT NULL,
question_id INT DEFAULT NULL,
quiz_id INT DEFAULT NULL,
datetime DATETIME NOT NULL,
action_status SMALLINT NOT NULL,
discr VARCHAR(255) NOT NULL,
INDEX IDX_A5C4B9FFA76ED395 (user_id),
INDEX IDX_A5C4B9FFDBF72317 (lp_item_id),
INDEX IDX_A5C4B9FF68DFD1EF (lp_id),
INDEX IDX_A5C4B9FF1E27F6BF (question_id),
INDEX IDX_A5C4B9FF853CD175 (quiz_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB";
Database::query($sql);
$sql = "ALTER TABLE whispeak_log_event ADD CONSTRAINT FK_A5C4B9FFA76ED395
FOREIGN KEY (user_id) REFERENCES user (id)";
Database::query($sql);
$sql = "ALTER TABLE whispeak_log_event ADD CONSTRAINT FK_A5C4B9FFDBF72317
FOREIGN KEY (lp_item_id) REFERENCES c_lp_item (iid)";
Database::query($sql);
$sql = "ALTER TABLE whispeak_log_event ADD CONSTRAINT FK_A5C4B9FF68DFD1EF
FOREIGN KEY (lp_id) REFERENCES c_lp (iid)";
Database::query($sql);
$sql = "ALTER TABLE whispeak_log_event ADD CONSTRAINT FK_A5C4B9FF1E27F6BF
FOREIGN KEY (question_id) REFERENCES c_quiz_question (iid)";
Database::query($sql);
$sql = "ALTER TABLE whispeak_log_event ADD CONSTRAINT FK_A5C4B9FF853CD175
FOREIGN KEY (quiz_id) REFERENCES c_quiz (iid)";
Database::query($sql);
}
}
/**
* Uninstall extra fields for user, learning path and quiz question.
*/
private function uninstallExtraFields()
{
$em = Database::getManager();
$authIdExtrafield = self::getAuthUidExtraField();
if (!empty($authIdExtrafield)) {
$em
->createQuery('DELETE FROM ChamiloCoreBundle:ExtraFieldValues efv WHERE efv.field = :field')
->execute(['field' => $authIdExtrafield]);
$em->remove($authIdExtrafield);
$em->flush();
}
$lpItemExtrafield = self::getLpItemExtraField();
if (!empty($lpItemExtrafield)) {
$em
->createQuery('DELETE FROM ChamiloCoreBundle:ExtraFieldValues efv WHERE efv.field = :field')
->execute(['field' => $lpItemExtrafield]);
$em->remove($lpItemExtrafield);
$em->flush();
}
$quizQuestionExtrafield = self::getQuizQuestionExtraField();
if (!empty($quizQuestionExtrafield)) {
$em
->createQuery('DELETE FROM ChamiloCoreBundle:ExtraFieldValues efv WHERE efv.field = :field')
->execute(['field' => $quizQuestionExtrafield]);
$em->remove($quizQuestionExtrafield);
$em->flush();
}
}
/**
* Uninstall the Doctrine's entities.
*/
private function uninstallEntities()
{
$pluginEntityPath = $this->getEntityPath();
$fs = new Filesystem();
if ($fs->exists($pluginEntityPath)) {
$fs->remove($pluginEntityPath);
}
$table = Database::get_main_table('whispeak_log_event');
$sql = "DROP TABLE IF EXISTS $table";
Database::query($sql);
}
/**
* @return string
*/
public function getApiUrl()
{
$url = $this->get(self::SETTING_API_URL);
return trim($url, " \t\n\r \v/").'/';
}
}

@ -0,0 +1,61 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class WhispeakConditionalLoginHook.
*
* Implements a Two-Factor Authentication with Whispeak.
*/
class WhispeakConditionalLoginHook extends HookObserver implements HookConditionalLoginObserverInterface
{
/**
* WhispeakConditionalLoginHook constructor.
*/
protected function __construct()
{
parent::__construct(
'plugin/whispeakauth/WhispeakAuthPlugin.php',
'whispeakauth'
);
}
/**
* Return an associative array (callable, url) needed for Conditional Login.
* <code>
* [
* 'conditional_function' => function (array $userInfo) {},
* 'url' => '',
* ]
* </code>
* conditional_function returns false to redirect to the url and returns true to continue with the classical login.
*
* @param HookConditionalLoginEventInterface $hook
*
* @return array
*/
public function hookConditionalLogin(HookConditionalLoginEventInterface $hook)
{
return [
'conditional_function' => function (array $userInfo) {
$isEnrolled = WhispeakAuthPlugin::checkUserIsEnrolled($userInfo['user_id']);
if (!$isEnrolled) {
return true;
}
$user2fa = (int) ChamiloSession::read(WhispeakAuthPlugin::SESSION_2FA_USER, 0);
if ($user2fa === (int) $userInfo['user_id']) {
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_2FA_USER);
return true;
}
ChamiloSession::write(WhispeakAuthPlugin::SESSION_2FA_USER, $userInfo['user_id']);
return false;
},
'url' => api_get_path(WEB_PLUGIN_PATH).$this->getPluginName().'/authentify.php',
];
}
}

@ -0,0 +1,63 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class WhispeakMyStudentsLpTrackingHook.
*/
class WhispeakMyStudentsLpTrackingHook extends HookObserver implements HookMyStudentsLpTrackingObserverInterface
{
/**
* WhispeakMyStudentsLpTrackingHook constructor.
*/
protected function __construct()
{
parent::__construct(
'plugin/whispeakauth/WhispeakAuthPlugin.php',
'whispeakauth'
);
}
/**
* @param HookMyStudentsLpTrackingEventInterface $hook
*
* @return array
*/
public function trackingHeader(HookMyStudentsLpTrackingEventInterface $hook)
{
return [
'value' => WhispeakAuthPlugin::create()->get_lang('plugin_title'),
'attrs' => ['class' => 'text-center'],
];
}
/**
* @param HookMyStudentsLpTrackingEventInterface $hook
*
* @throws \Doctrine\ORM\Query\QueryException
*
* @return array
*/
public function trackingContent(HookMyStudentsLpTrackingEventInterface $hook)
{
$data = $hook->getEventData();
$totalCount = WhispeakAuthPlugin::countAllAttemptsInLearningPath($data['lp_id'], $data['student_id']);
if (0 === $totalCount) {
return [
'value' => '-',
'attrs' => ['class' => 'text-center'],
];
}
$successCount = WhispeakAuthPlugin::countSuccessAttemptsInLearningPath($data['lp_id'], $data['student_id']);
$attrs = ['class' => 'text-center '];
$attrs['class'] .= $successCount <= $totalCount / 2 ? 'text-danger' : 'text-success';
return [
'value' => Display::tag('strong', "$successCount / $totalCount"),
'attrs' => $attrs,
];
}
}

@ -0,0 +1,81 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class WhispeakMyStudentsQuizTrackingHook.
*/
class WhispeakMyStudentsQuizTrackingHook extends HookObserver implements HookMyStudentsQuizTrackingObserverInterface
{
/**
* WhispeakMyStudentsQuizTrackingHook constructor.
*/
protected function __construct()
{
parent::__construct(
'plugin/whispeakauth/WhispeakAuthPlugin.php',
'whispeakauth'
);
}
/**
* Return an associative array this value and attributes.
* <code>
* [
* 'value' => 'Users online',
* 'attrs' => ['class' => 'text-center'],
* ]
* </code>.
*
* @param HookMyStudentsQuizTrackingEventInterface $hook
*
* @return array
*/
public function trackingHeader(HookMyStudentsQuizTrackingEventInterface $hook)
{
return [
'value' => WhispeakAuthPlugin::create()->get_lang('plugin_title'),
'attrs' => [
'class' => 'text-center',
],
];
}
/**
* Return an associative array this value and attributes.
* <code>
* [
* 'value' => '5 connected users ',
* 'attrs' => ['class' => 'text-center text-success'],
* ]
* </code>.
*
* @param HookMyStudentsQuizTrackingEventInterface $hook
*
* @throws \Doctrine\ORM\Query\QueryException
*
* @return array
*/
public function trackingContent(HookMyStudentsQuizTrackingEventInterface $hook)
{
$data = $hook->getEventData();
$totalCount = WhispeakAuthPlugin::countAllAttemptsInQuiz($data['quiz_id'], $data['student_id']);
if (0 === $totalCount) {
return [
'value' => '-',
'attrs' => ['class' => 'text-center'],
];
}
$successCount = WhispeakAuthPlugin::countSuccessAttemptsInQuiz($data['quiz_id'], $data['student_id']);
$attrs = ['class' => 'text-center '];
$attrs['class'] .= $successCount <= $totalCount / 2 ? 'text-danger' : 'text-success';
return [
'value' => Display::tag('strong', "$successCount / $totalCount"),
'attrs' => $attrs,
];
}
}

@ -0,0 +1,128 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\WhispeakAuth\LogEvent;
use Chamilo\PluginBundle\Entity\WhispeakAuth\LogEventLp;
use Chamilo\PluginBundle\Entity\WhispeakAuth\LogEventQuiz;
$cidReset = true;
require_once __DIR__.'/../../main/inc/global.inc.php';
$plugin = WhispeakAuthPlugin::create();
api_protect_admin_script(true);
$plugin->protectTool();
$form = new FormValidator('frm_filter', 'GET');
$slctUsers = $form->addSelectAjax(
'users',
get_lang('Users'),
[],
[
'url' => api_get_path(WEB_AJAX_PATH).'user_manager.ajax.php?a=get_user_like',
'id' => 'user_id',
'multiple' => true,
]
);
$form->addDatePicker('date', get_lang('Date'));
$form->addButtonSearch(get_lang('Search'));
$results = [];
if ($form->validate()) {
$formValues = $form->exportValues();
$userIds = $formValues['users'] ?: [];
/** @var \DateTime $date */
$starDate = api_get_utc_datetime($formValues['date'], true, true);
$endDate = clone $starDate;
$endDate->modify('next day');
$em = Database::getManager();
$repo = $em->getRepository('ChamiloPluginBundle:WhispeakAuth\LogEvent');
foreach ($userIds as $userId) {
$qb = $em->createQueryBuilder();
$results[$userId] = $qb
->select('event')
->from('ChamiloPluginBundle:WhispeakAuth\LogEvent', 'event')
->where(
$qb->expr()->gte('event.datetime', ':start_date')
)
->andWhere(
$qb->expr()->lt('event.datetime', ':end_date')
)
->andWhere(
$qb->expr()->eq('event.user', ':user')
)
->setParameters(
[
'start_date' => $starDate->format('Y-m-d H:i:s'),
'end_date' => $endDate->format('Y-m-d H:i:s'),
'user' => $userId,
]
)
->getQuery()
->getResult();
}
}
$pageContent = '';
/**
* @var int $userId
* @var array|LogEvent[] $logEvents
*/
foreach ($results as $userId => $logEvents) {
if (empty($logEvents)) {
continue;
}
$user = $logEvents[0]->getUser();
$slctUsers->addOption($user->getCompleteNameWithUsername(), $user->getId());
$table = new HTML_Table(['class' => 'table table-hover']);
$table->setHeaderContents(0, 0, get_lang('DateTime'));
$table->setHeaderContents(0, 1, get_lang('Type'));
$table->setHeaderContents(0, 2, get_lang('Status'));
foreach ($logEvents as $i => $logEvent) {
$row = $i + 1;
$type = '';
switch (get_class($logEvent)) {
case LogEventQuiz::class:
$type = '<span class="label label-info">'.get_lang('Question').'</span>'.PHP_EOL;
break;
case LogEventLp::class:
$type = '<span class="label label-info">'.get_lang('LearningPath').'</span>'.PHP_EOL;
break;
}
$table->setCellContents(
$row,
0,
[
api_convert_and_format_date($logEvent->getDatetime(), DATE_TIME_FORMAT_SHORT),
$type.PHP_EOL.$logEvent->getTypeString(),
$logEvent->getActionStatus() === LogEvent::STATUS_SUCCESS ? get_lang('Success') : get_lang('Failed'),
]
);
}
$table->updateColAttributes(0, ['class' => 'text-center']);
$table->updateColAttributes(2, ['class' => 'text-center']);
$pageContent .= Display::page_header($user->getCompleteNameWithUsername());
$pageContent .= $table->toHtml();
}
$template = new Template($plugin->get_title());
$template->assign(
'content',
$form->returnForm().PHP_EOL.$pageContent
);
$template->display_one_col_template();

@ -0,0 +1,142 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\WhispeakAuth\LogEvent;
$cidReset = true;
require_once __DIR__.'/../../../main/inc/global.inc.php';
api_block_anonymous_users(false);
$plugin = WhispeakAuthPlugin::create();
$plugin->protectTool(false);
$tokenIsValid = Security::check_token();
if (!$tokenIsValid) {
WhispeakAuthPlugin::displayNotAllowedMessage();
}
$maxAttempts = $plugin->getMaxAttempts();
$failedLogins = ChamiloSession::read(WhispeakAuthPlugin::SESSION_FAILED_LOGINS, 0);
if ($maxAttempts && $failedLogins >= $maxAttempts) {
echo Display::return_message($plugin->get_lang('MaxAttemptsReached'), 'warning');
exit;
}
$user = api_get_user_entity(api_get_user_id());
$password = isset($_POST['password']) ? $_POST['password'] : null;
if (empty($password) || empty($user)) {
WhispeakAuthPlugin::displayNotAllowedMessage();
}
if (!in_array($user->getAuthSource(), [PLATFORM_AUTH_SOURCE, CAS_AUTH_SOURCE])) {
WhispeakAuthPlugin::displayNotAllowedMessage();
}
/** @var array $lpItemInfo */
$lpItemInfo = ChamiloSession::read(WhispeakAuthPlugin::SESSION_LP_ITEM, []);
/** @var array $quizQuestionInfo */
$quizQuestionInfo = ChamiloSession::read(WhispeakAuthPlugin::SESSION_QUIZ_QUESTION, []);
$isValidPassword = UserManager::isPasswordValid($user->getPassword(), $password, $user->getSalt());
$isActive = $user->isActive();
$isExpired = empty($user->getExpirationDate()) || $user->getExpirationDate() > api_get_utc_datetime(null, false, true);
$userPass = true;
if (!$isValidPassword || !$isActive || !$isExpired) {
if (!empty($lpItemInfo)) {
$plugin->addAttemptInLearningPath(
LogEvent::STATUS_FAILED,
$user->getId(),
$lpItemInfo['lp_item'],
$lpItemInfo['lp']
);
} elseif (!empty($quizQuestionInfo)) {
$plugin->addAttemptInQuiz(
LogEvent::STATUS_FAILED,
$user->getId(),
$quizQuestionInfo['question'],
$quizQuestionInfo['quiz']
);
}
$userPass = false;
$message = $plugin->get_lang('AuthentifyFailed');
if (!$isActive) {
$message .= PHP_EOL.get_lang('Account inactive');
}
if (!$isExpired) {
$message .= PHP_EOL.get_lang('AccountExpired');
}
ChamiloSession::write(WhispeakAuthPlugin::SESSION_FAILED_LOGINS, ++$failedLogins);
if ($maxAttempts && $failedLogins >= $maxAttempts) {
$message .= PHP_EOL.'<span data-reach-attempts="true">'.$plugin->get_lang('MaxAttemptsReached').'</span>';
} else {
$message .= PHP_EOL.$plugin->get_lang('TryAgain');
}
echo Display::return_message($message, 'error', false);
if (!$maxAttempts ||
($maxAttempts && $failedLogins >= $maxAttempts)
) {
$userPass = true;
}
} elseif ($isValidPassword) {
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_FAILED_LOGINS);
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_2FA_USER);
if (!empty($lpItemInfo)) {
$plugin->addAttemptInLearningPath(
LogEvent::STATUS_SUCCESS,
$user->getId(),
$lpItemInfo['lp_item'],
$lpItemInfo['lp']
);
} elseif (!empty($quizQuestionInfo)) {
$plugin->addAttemptInQuiz(
LogEvent::STATUS_SUCCESS,
$user->getId(),
$quizQuestionInfo['question'],
$quizQuestionInfo['quiz']
);
}
echo Display::return_message($plugin->get_lang('AuthentifySuccess'), 'success');
}
if ($userPass) {
$url = '';
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_FAILED_LOGINS);
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_2FA_USER);
if ($lpItemInfo) {
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_LP_ITEM);
$url = $lpItemInfo['src'];
} elseif ($quizQuestionInfo) {
$quizQuestionInfo['passed'] = true;
$url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit.php?'.$quizQuestionInfo['url_params'];
ChamiloSession::write(WhispeakAuthPlugin::SESSION_QUIZ_QUESTION, $quizQuestionInfo);
}
if (!empty($url)) {
echo '
<script>window.location.href = "'.$url.'";</script>
';
}
}

@ -1,9 +1,8 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\UserBundle\Entity\User;
use FFMpeg\FFMpeg;
use FFMpeg\Format\Audio\Wav;
use Chamilo\PluginBundle\WhispeakAuth\Controller\AuthenticationController;
use Chamilo\PluginBundle\WhispeakAuth\Controller\EnrollmentController;
$cidReset = true;
@ -13,124 +12,29 @@ $action = isset($_POST['action']) ? $_POST['action'] : 'enrollment';
$isEnrollment = 'enrollment' === $action;
$isAuthentify = 'authentify' === $action;
$isAllowed = true;
$isAllowed = false;
if ($isEnrollment) {
api_block_anonymous_users(false);
$isAllowed = !empty($_FILES['audio']);
} elseif ($isAuthentify) {
$isAllowed = !empty($_POST['username']) && !empty($_FILES['audio']);
}
if (!$isAllowed) {
echo Display::return_message(get_lang('NotAllowed'), 'error');
exit;
}
$plugin = WhispeakAuthPlugin::create();
$plugin->protectTool(false);
if ($isAuthentify) {
$em = Database::getManager();
/** @var User|null $user */
$user = $em->getRepository('ChamiloUserBundle:User')->findOneBy(['username' => $_POST['username']]);
} else {
/** @var User $user */
$user = api_get_user_entity(api_get_user_id());
}
if (empty($user)) {
echo Display::return_message(get_lang('NoUser'), 'error');
exit;
}
$path = api_upload_file('whispeakauth', $_FILES['audio'], $user->getId());
if (false === $path) {
echo Display::return_message(get_lang('UploadError'), 'error');
exit;
}
$newFullPath = $originFullPath = api_get_path(SYS_UPLOAD_PATH).'whispeakauth'.$path['path_to_save'];
$fileType = mime_content_type($originFullPath);
if ('wav' !== substr($fileType, -3)) {
$directory = dirname($originFullPath);
$newFullPath = $directory.'/audio.wav';
$controller = new EnrollmentController();
try {
$ffmpeg = FFMpeg::create();
$audio = $ffmpeg->open($originFullPath);
$audio->save(new Wav(), $newFullPath);
$controller->ajax();
} catch (Exception $exception) {
echo Display::return_message($exception->getMessage(), 'error');
exit;
WhispeakAuthPlugin::displayNotAllowedMessage(
$exception->getMessage()
);
}
}
if ($isEnrollment) {
$result = $plugin->requestEnrollment($user, $newFullPath);
if (empty($result)) {
echo Display::return_message($plugin->get_lang('EnrollmentFailed'));
exit;
}
$reliability = (int) $result['reliability'];
if ($reliability <= 0) {
echo Display::return_message($plugin->get_lang('EnrollmentSignature0'), 'error');
exit;
}
$plugin->saveEnrollment($user, $result['uid']);
$message = '<strong>'.$plugin->get_lang('EnrollmentSuccess').'</strong>';
$message .= PHP_EOL;
$message .= $plugin->get_lang("EnrollmentSignature$reliability");
echo Display::return_message($message, 'success', false);
exit;
die;
}
if ($isAuthentify) {
$result = $plugin->requestAuthentify($user, $newFullPath);
$controller= new AuthenticationController();
if (empty($result)) {
echo Display::return_message($plugin->get_lang('AuthentifyFailed'), 'error');
exit;
}
$success = (bool) $result['audio'][0]['result'];
if (!$success) {
echo Display::return_message($plugin->get_lang('TryAgain'), 'warning');
exit;
try {
$controller->ajax();
} catch (Exception $exception) {
echo Display::return_message($exception->getMessage(), 'error', false);
}
$loggedUser = [
'user_id' => $user->getId(),
'status' => $user->getStatus(),
'uidReset' => true,
];
ChamiloSession::write('_user', $loggedUser);
Login::init_user($user->getId(), true);
echo Display::return_message($plugin->get_lang('AuthentifySuccess'), 'success');
echo '<script>window.location.href = "'.api_get_path(WEB_PATH).'";</script>';
exit;
}

@ -9,7 +9,6 @@ window.RecordAudio = (function () {
recordRTC = null,
btnStart = $(rtcInfo.btnStartId),
btnStop = $(rtcInfo.btnStopId),
btnSave = $(rtcInfo.btnSaveId),
tagAudio = $(rtcInfo.plyrPreviewId);
function saveAudio() {
@ -19,7 +18,7 @@ window.RecordAudio = (function () {
return;
}
var btnSaveText = btnSave.html();
var btnStopText = btnStop.html();
var fileExtension = recordedBlob.type.split('/')[1];
var formData = new FormData();
@ -41,15 +40,28 @@ window.RecordAudio = (function () {
type: 'POST',
beforeSend: function () {
btnStart.prop('disabled', true);
btnStop.prop('disabled', true);
btnSave.prop('disabled', true).text(btnSave.data('loadingtext'));
btnStop.prop('disabled', true).text(btnStop.data('loadingtext'));
}
}).done(function (response) {
$('#messages-deck').append(response);
}).always(function () {
btnSave.prop('disabled', true).html(btnSaveText).parent().addClass('hidden');
btnStop.prop('disabled', true).parent().addClass('hidden');
btnStart.prop('disabled', false).parent().removeClass('hidden');
$('#messages-deck').html(response);
if ($('#messages-deck > .alert.alert-success').length > 0) {
tagAudio.parents('#audio-wrapper').addClass('hidden').removeClass('show');
} else {
tagAudio.parents('#audio-wrapper').removeClass('hidden').addClass('show');
}
btnStop.prop('disabled', true).html(btnStopText).parent().addClass('hidden');
if ($('#messages-deck > .alert.alert-success').length > 0 ||
$('#messages-deck > .alert.alert-warning [data-reach-attempts]').length > 0
) {
btnStart.prop('disabled', true);
} else {
btnStart.prop('disabled', false);
}
btnStart.parent().removeClass('hidden');
});
}
@ -66,7 +78,6 @@ window.RecordAudio = (function () {
});
recordRTC.startRecording();
btnSave.prop('disabled', true).parent().addClass('hidden');
btnStop.prop('disabled', false).parent().removeClass('hidden');
btnStart.prop('disabled', true).parent().addClass('hidden');
tagAudio.removeClass('show').parents('#audio-wrapper').addClass('hidden');
@ -97,26 +108,12 @@ window.RecordAudio = (function () {
}
recordRTC.stopRecording(function (audioURL) {
btnStart.prop('disabled', false).parent().removeClass('hidden');
btnStop.prop('disabled', true).parent().addClass('hidden');
btnSave.prop('disabled', false).parent().removeClass('hidden');
tagAudio
.prop('src', audioURL)
.parents('#audio-wrapper')
.removeClass('hidden')
.addClass('show');
tagAudio.prop('src', audioURL);
localStream.getTracks()[0].stop();
});
});
btnSave.on('click', function () {
if (!recordRTC) {
return;
}
saveAudio();
saveAudio();
});
});
}
@ -135,5 +132,5 @@ window.RecordAudio = (function () {
useRecordRTC(rtcInfo);
}
}
};
})();

@ -1,26 +1,19 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\WhispeakAuth\Controller\AuthenticationController;
$cidReset = true;
require_once __DIR__.'/../../main/inc/global.inc.php';
$plugin = WhispeakAuthPlugin::create();
$plugin->protectTool();
$form = new FormValidator('enter_username', 'post', '#');
$form->addText('username', get_lang('Username'));
$htmlHeadXtra[] = api_get_js('rtc/RecordRTC.js');
$htmlHeadXtra[] = api_get_js_simple(api_get_path(WEB_PLUGIN_PATH).'whispeakauth/assets/js/RecordAudio.js');
$template = new Template();
$template->assign('form', $form->returnForm());
$template->assign('sample_text', $plugin->getAuthentifySampleText());
$content = $template->fetch('whispeakauth/view/authentify_recorder.html.twig');
$controller = new AuthenticationController();
$template->assign('header', $plugin->get_title());
$template->assign('content', $content);
$template->display_one_col_template();
try {
$controller->index();
} catch (Exception $exception) {
api_not_allowed(
true,
Display::return_message($exception->getMessage(), 'warning')
);
}

@ -0,0 +1,79 @@
<?php
/* For licensing terms, see /license.txt */
$cidReset = true;
require_once __DIR__.'/../../main/inc/global.inc.php';
$plugin = WhispeakAuthPlugin::create();
$plugin->protectTool();
$userId = ChamiloSession::read(WhispeakAuthPlugin::SESSION_2FA_USER, 0) ?: api_get_user_id();
/** @var array $lpItemInfo */
$lpItemInfo = ChamiloSession::read(WhispeakAuthPlugin::SESSION_LP_ITEM, []);
/** @var learnpath $oLp */
$oLp = ChamiloSession::read('oLP', null);
/** @var array $lpQuestionInfo */
$lpQuestionInfo = ChamiloSession::read(WhispeakAuthPlugin::SESSION_QUIZ_QUESTION, []);
/** @var Exercise $objExercise */
$objExercise = ChamiloSession::read('objExercise', null);
$isAuthOnLp = !empty($lpItemInfo) && !empty($oLp);
$isAuthOnQuiz = !empty($lpQuestionInfo) && !empty($objExercise);
$showFullPage = !$isAuthOnLp && !$isAuthOnQuiz;
if (empty($userId)) {
api_not_allowed($showFullPage);
}
if (!empty($lpQuestionInfo) && empty($lpItemInfo)) {
echo Display::return_message(
$plugin->get_lang('MaxAttemptsReached').'<br>'
.'<strong>'.$plugin->get_lang('LoginWithUsernameAndPassword').'</strong>',
'warning',
false
);
}
$form = new FormValidator(
'form-login',
'POST',
api_get_path(WEB_PLUGIN_PATH).'whispeakauth/ajax/authentify_password.php',
null,
null,
FormValidator::LAYOUT_BOX_NO_LABEL
);
$form->addElement(
'password',
'password',
get_lang('Pass'),
['id' => 'password', 'icon' => 'lock fa-fw', 'placeholder' => get_lang('Pass')]
);
$form->addHidden('sec_token', '');
$form->setConstants(['sec_token' => Security::get_token()]);
$form->addButton('submitAuth', get_lang('LoginEnter'), 'check', 'primary', 'default', 'btn-block');
$template = new Template(
!$showFullPage ? '' : $plugin->get_title(),
$showFullPage,
$showFullPage,
false,
true,
false
);
$template->assign('form', $form->returnForm());
$content = $template->fetch('whispeakauth/view/authentify_password.html.twig');
if (!empty($lpQuestionInfo) && empty($lpItemInfo)) {
echo $content;
exit;
}
$template->assign('header', $plugin->get_title());
$template->assign('content', $content);
$template->display_one_col_template();

@ -1,28 +1,21 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\WhispeakAuth\Controller\EnrollmentController;
$cidReset = true;
require_once __DIR__.'/../../main/inc/global.inc.php';
api_block_anonymous_users(true);
$userId = api_get_user_id();
$plugin = WhispeakAuthPlugin::create();
$plugin->protectTool();
$sampleText = $plugin->get_lang('EnrollmentSampleText');
$htmlHeadXtra[] = api_get_js('rtc/RecordRTC.js');
$htmlHeadXtra[] = api_get_js_simple(api_get_path(WEB_PLUGIN_PATH).'whispeakauth/assets/js/RecordAudio.js');
$template = new Template();
$template->assign('is_authenticated', WhispeakAuthPlugin::checkUserIsEnrolled($userId));
$template->assign('sample_text', $sampleText);
$content = $template->fetch('whispeakauth/view/record_audio.html.twig');
$controller = new EnrollmentController();
$template->assign('header', $plugin->get_title());
$template->assign('content', $content);
$template->display_one_col_template();
try {
$controller->index();
} catch (Exception $exception) {
api_not_allowed(
true,
Display::return_message($exception->getMessage(), 'warning')
);
}

@ -5,11 +5,14 @@ $strings['plugin_title'] = 'Speech authentication with Whispeak';
$strings['plugin_comment'] = 'Allow speech authentication in Chamilo.';
$strings['enable'] = 'Enable';
$strings['enable_help'] = 'Add <code>$_configuration[\'whispeak_auth_enabled\'] = true;</code> in the <code>configuration.php</code> file';
$strings['api_url'] = 'API URL';
$strings['api_url_help'] = 'http://api.whispeak.io:8080/v1/';
$strings['token'] = 'API key';
$strings['instruction'] = '<p>Add <code>$_configuration[\'whispeak_auth_enabled\'] = true;</code>'.
'in the <code>configuration.php</code> file</p>';
$strings['max_attempts'] = 'Max attempts';
$strings['max_attempts_help'] = '(Optional) If the Whispeak authentication is failed x times, then ask and verify the password of the user';
$strings['2fa'] = 'Two-Factor Authentication';
$strings['2fa_help'] = 'Allows extend the login page with a Two-Factor Authentication process. After the classic login, the user must authenticate through Whispeak.';
$strings['EnrollmentSampleText'] = 'The famous Mona Lisa painting was painted by Leonardo Da Vinci.';
$strings['AuthentifySampleText1'] = 'Dropping Like Flies.';
@ -25,9 +28,30 @@ $strings['EnrollmentSignature1'] = 'Passable signature, advice to make a new enr
$strings['EnrollmentSignature2'] = 'Correct signature.';
$strings['EnrollmentSignature3'] = 'Good signature.';
$strings['SpeechAuthAlreadyEnrolled'] = 'Speech authentication already enrolled previously.';
$strings['SpeechAuthNotEnrolled'] = 'Speech authentication not enrolled previously.';
$strings['SpeechAuthentication'] = 'Speech authentication';
$strings['EnrollmentFailed'] = 'Enrollment failed.';
$strings['EnrollmentSuccess'] = 'Enrollment success.';
$strings['AuthentifyFailed'] = 'Login failed.';
$strings['AuthentifySuccess'] = 'Authentication success!';
$strings['TryAgain'] = 'Try again';
$strings['MaxAttemptsReached'] = 'You reached the maximum number of attempts allowed.';
$strings['LoginWithUsernameAndPassword'] = 'You should login using the username and password.';
$strings['YouNeedToIdentifyYourselfToAnswerThisQuestion'] = 'You need to identify yourself to answer this question.';
$strings['IdentifyMe'] = 'Identify me';
$strings['PleaseWaitWhileLoading'] = "Please wait while loading...";
$strings['Quality'] = 'Quality';
$strings['Failed'] = "Failed";
$strings['ActivityId'] = "Activity ID";
$strings['Success'] = "Success";
$strings['AudioQualityShort'] = 'Too short audio';
$strings['AudioQualityQuiet'] = 'Too quiet audio';
$strings['AudioQualityLoud'] = 'Too loud audio';
$strings['AudioQualityNoisy'] = 'Too noisy audio';
$strings['AudioQualityFrequency'] = 'Missing some audio frequencies';
$strings['AudioQualityPoorness'] = 'Too poor general audio quality';
$strings['AgreeAllowResearch'] = 'I agree to allow the use of data for research (no commercial usage).';
$strings['MarkForSpeechAuthentication'] = 'Mark it for speech authentication';

@ -2,17 +2,56 @@
/* For licensing terms, see /license.txt */
$strings['plugin_title'] = 'Authentification vocale avec Whispeak';
$strings['plugin_comment'] = 'Autoriser l\'authentification de la voix dans Chamilo.';
$strings['enable'] = 'Activer';
$strings['enable_help'] = '<p>Ajoutez <code>$_configuration[\'whispeak_auth_enabled\'] = true;</code> dans le fichier <code>configuration.php</code></p>';
$strings['api_url'] = 'URL de l\'API';
$strings['api_url_help'] = 'http://api.whispeak.io:8080/v1.1/';
$strings['token'] = 'Clef API';
$strings['max_attempts'] = 'Tentatives maximum';
$strings['max_attempts_help'] = '(Optionnel) Si l\'authentification Whispeak échoue x fois, alors abandonner et demander le mot de passe de l\'utilisateur';
$strings['2fa'] = 'Authentification à 2 facteurs (2FA)';
$strings['2fa_help'] = 'Autoriser l\'extension du formulaire de login par une page d\'authentification forte. Après le login classique, l\'utilisateur/trice devra aussi s\'authentifier au travers de Whispeak.';
$strings['EnrollmentSampleText'] = 'Le fameux chef-d\'oeuvre Mona Lisa a été peint par Léonardo da Vinci.';
$strings['AuthentifySampleText1'] = 'Tomber comme des mouches.';
$strings['AuthentifySampleText2'] = 'Restez vigilants.';
$strings['AuthentifySampleText3'] = 'Le renard hurle à minuit.';
$strings['AuthentifySampleText4'] = 'Errer dans la campagne.';
$strings['AuthentifySampleText5'] = 'Sous l\'océan.';
$strings['AuthentifySampleText6'] = 'Prendre la mouche.';
$strings['RepeatThisPhrase'] = 'Autorisez l\'enregistrement audio puis répétez cette phrase trois fois:';
$strings['EnrollmentSignature0'] = 'Signature non viable, nécessite un nouvel enrôlement';
$strings['EnrollmentSignature1'] = 'Signature passable, conseil de faire un nouvel enrôlement.';
$strings['RepeatThisPhrase'] = 'Autorisez l\'enregistrement audio puis lisez cette phrase à voix haute :';
$strings['EnrollmentSignature0'] = 'Signature non utilisable, nécessite un nouvel enregistrement';
$strings['EnrollmentSignature1'] = 'Signature passable, il est conseillé de faire un nouvel enregistrement.';
$strings['EnrollmentSignature2'] = 'Signature correcte.';
$strings['EnrollmentSignature3'] = 'Signature bonne.';
$strings['SpeechAuthentication'] = 'Authentification de voix';
$strings['EnrollmentSignature3'] = 'Signature satisfaisante.';
$strings['SpeechAuthAlreadyEnrolled'] = 'L\'authentification de voix a déjà réussi précédemment.';
$strings['SpeechAuthNotEnrolled'] = 'L\'authentification de voix n\'a pas encore été enregistrée.';
$strings['SpeechAuthentication'] = 'Authentification par la voix';
$strings['EnrollmentFailed'] = 'Échec à l\'inscription.';
$strings['EnrollmentSuccess'] = 'Inscription réussie.';
$strings['AuthentifyFailed'] = 'Échec de l\'authentification.';
$strings['AuthentifySuccess'] = 'Authentification réussie!';
$strings['TryAgain'] = 'Essayez encore';
$strings['MaxAttemptsReached'] = 'Vous avez atteint le nombre maximum de tentatives autorisées.';
$strings['LoginWithUsernameAndPassword'] = 'Authentifiez-vous en utilisant votre mot de passe.';
$strings['YouNeedToIdentifyYourselfToAnswerThisQuestion'] = 'Vous devez vous authentifier pour répondre à cette question.';
$strings['IdentifyMe'] = 'M\'identifier';
$strings['PleaseWaitWhileLoading'] = 'Connexion au serveur d\'authentification. Veuillez patienter...';
$strings['Quality'] = 'Quality';
$strings['Failed'] = "Failed";
$strings['ActivityId'] = "Activity ID";
$strings['Success'] = "Success";
$strings['AudioQualityShort'] = 'Audio trop court';
$strings['AudioQualityQuiet'] = 'Audio trop peu audible';
$strings['AudioQualityLoud'] = 'Audio trop fort';
$strings['AudioQualityNoisy'] = 'Audio trop bruyant';
$strings['AudioQualityFrequency'] = 'Certaines fréquences audio sont manquantes';
$strings['AudioQualityPoorness'] = 'Qualité générale de l\'enregistrement trop faible';
$strings['AgreeAllowResearch'] = 'J\'accepte l\'utilisation de mes données à des fins de recherche (non commerciales).';
$strings['MarkForSpeechAuthentication'] = 'Cocher pour l\'authentification par la voix';

@ -5,11 +5,14 @@ $strings['plugin_title'] = 'Authenticación de voz con Whispeak';
$strings['plugin_comment'] = 'Permitir autenticación de voz en Chamilo.';
$strings['enable'] = 'Habilitar';
$strings['enable_help'] = '<p>Agrega <code>$_configuration[\'whispeak_auth_enabled\'] = true;</code> al archivo <code>configuration.php</code></p>';
$strings['api_url'] = 'URL del API';
$strings['api_url_help'] = 'http://api.whispeak.io:8080/v1/';
$strings['token'] = 'Llave del API';
$strings['instruction'] = '<p>Agrega <code>$_configuration[\'whispeak_auth_enabled\'] = true;</code>'.
'al archivo <code>configuration.php</code></p>';
$strings['max_attempts'] = 'Máximo de intentos';
$strings['max_attempts_help'] = '(Opcional) Si la autenticación de Whispeak falla x intentos, preguntar y verificar la contraseña del usuario';
$strings['2fa'] = 'Autenticación en dos factores';
$strings['2fa_help'] = 'Permite extender la página de inicio de sesión con un proceso de dos factores. Después del inicio de sesión clásico, el usuario deberá autenticarse a través de Whispeak.';
$strings['EnrollmentSampleText'] = 'El famoso cuadro de Mona Lisa fue pintado por Leonardo Da Vinci.';
$strings['AuthentifySampleText1'] = 'Cayendo como moscas.';
@ -25,9 +28,30 @@ $strings['EnrollmentSignature1'] = 'Firma aceptable, pero se aconseja hacer una
$strings['EnrollmentSignature2'] = 'Firma correcta.';
$strings['EnrollmentSignature3'] = 'Buena firma.';
$strings['SpeechAuthAlreadyEnrolled'] = 'Autenticación de voz registrada anteriormente.';
$strings['SpeechAuthNotEnrolled'] = 'Autenticación de voz no registrada previamente.';
$strings['SpeechAuthentication'] = 'Atenticación con voz';
$strings['EnrollmentFailed'] = 'Inscripción fallida.';
$strings['EnrollmentSuccess'] = 'Inscripción correcta.';
$strings['AuthentifyFailed'] = 'Inicio de sesión fallido.';
$strings['AuthentifySuccess'] = '¡Autenticación correcta!';
$strings['TryAgain'] = 'Intente de nuevo.';
$strings['MaxAttemptsReached'] = 'Ha alcanzado el número máximo de intentos permitidos.';
$strings['LoginWithUsernameAndPassword'] = 'Debe iniciar sesión usando su nombre de usuario y contraseña.';
$strings['YouNeedToIdentifyYourselfToAnswerThisQuestion'] = 'Necesita identificarse para responder esta pregunta.';
$strings['IdentifyMe'] = 'Identificarme';
$strings['PleaseWaitWhileLoading'] = "Por favor, espere mientras dure la carga...";
$strings['Quality'] = 'Calidad';
$strings['Failed'] = "Fallido";
$strings['ActivityId'] = "Identificador de actividad";
$strings['Success'] = "Satisfactotio";
$strings['AudioQualityShort'] = 'Audio demasiado corto';
$strings['AudioQualityQuiet'] = 'Audio demasiado silencioso';
$strings['AudioQualityLoud'] = 'Audio demasiado alto';
$strings['AudioQualityNoisy'] = 'Audio demasiado ruidoso';
$strings['AudioQualityFrequency'] = 'Falta algunas frecuencias de audio';
$strings['AudioQualityPoorness'] = 'Calidad de audio general demasiado pobre';
$strings['AgreeAllowResearch'] = 'Estoy de acuerdo en permitir el uso de datos para investigación (no uso comercial).';
$strings['MarkForSpeechAuthentication'] = 'Marcarlo para autenticación con voz';

@ -0,0 +1,44 @@
<div class="row">
<div class="col-sm-6 col-sm-offset-3 col-md-4 col-md-offset-4">
{{ form }}
<br>
<div id="frm-login-result"></div>
</div>
</div>
<script>
$(function () {
$('#form-login').on('submit', function (e) {
e.preventDefault();
var formData = new FormData(this),
self = $(this);
self.children().prop('disabled', true);
self.find(':submit em.fa').get(0).className = 'fa fa-spinner fa-spin';
$
.ajax({
type: 'POST',
url: this.action,
data: formData,
processData: false,
contentType: false
})
.then(function (response) {
$('#frm-login-result').html(response);
self.children().prop('disabled', false);
self.find(':submit em.fa').get(0).className = 'fa fa-check';
if ($('#frm-login-result > .alert.alert-success').length > 0 ||
$('#frm-login-result > .alert.alert-danger [data-reach-attempts]').length > 0
) {
self.find(':submit').prop('disabled', true);
}
});
this.reset();
});
});
</script>

@ -1,26 +1,29 @@
{% extends 'whispeakauth/view/record_audio.html.twig' %}
{% block intro %}
<form class="form-horizontal" action="#" method="post">
<div class="form-group ">
<label for="enter_username_username" class="col-sm-4 control-label">
{{ 'Username'|get_lang }}
</label>
<div class="col-sm-8">
<input class="form-control" name="username" type="text" id="username">
{% if show_form %}
<form class="form-horizontal" action="#" method="post">
<div class="form-group ">
<label for="enter_username_username" class="col-sm-4 control-label">
{{ 'Username'|get_lang }}
</label>
<div class="col-sm-8">
<input class="form-control" name="username" type="text" id="username">
</div>
</div>
</div>
</form>
</form>
<hr>
<hr>
{% endif %}
{{ parent() }}
{% endblock %}
{% block config_data %}
$('#username').on('change', function () {
$('#record-audio-recordrtc, #btn-start-record, #btn-stop-record, #btn-save-record').off('click', '');
{% if show_form %}
$('#username').on('change', function () {
$('#record-audio-recordrtc, #btn-start-record, #btn-stop-record, #btn-save-record').off('click', '');
{% endif %}
RecordAudio.init(
{
blockId: '#record-audio-recordrtc',
@ -34,5 +37,8 @@
}
}
);
});
{% if show_form %}
});
{% endif %}
{% endblock %}

@ -0,0 +1,45 @@
<div class="text-center">
<p>{{ 'YouNeedToIdentifyYourselfToAnswerThisQuestion'|get_plugin_lang('WhispeakAuthPlugin') }}</p>
<button type="button" class="btn btn-info" id="whispeak-question-{{ question }}" data-loading-text="{{ 'PleaseWaitWhileLoading'|get_plugin_lang('WhispeakAuthPlugin')|escape('html') }}">
{{ 'IdentifyMe'|get_plugin_lang('WhispeakAuthPlugin') }}
</button>
</div>
<script>
$(function () {
function loadAuth() {
var $btnTrigger = $('#whispeak-question-{{ question }}'),
originalText = $btnTrigger.text(),
$modal = $('#global-modal'),
$modalTitle = $modal.find('.modal-title'),
$modalBody = $modal.find('.modal-body'),
$originalLoadingText = $btnTrigger.data('loading-text');
$btnTrigger.text($originalLoadingText).prop('disabled', true);
$modalTitle.text($originalLoadingText);
$modalBody.html('');
$modal.modal('show');
$
.ajax({
url: _p.web_plugin + 'whispeakauth/authentify.php'
})
.then(function (response) {
$modalBody.html(response);
$modalTitle.text('{{ 'plugin_title'|get_plugin_lang('WhispeakAuthPlugin') }}');
$btnTrigger.text(originalText).prop('disabled', false);
});
}
$('#whispeak-question-{{ question }}').on('click', function (e) {
e.preventDefault();
loadAuth();
});
loadAuth();
});
</script>

@ -1,5 +1,5 @@
<div id="record-audio-recordrtc" class="row">
<div class="col-sm-6">
<div class="col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-0">
{% block intro %}
<p class="text-center">{{ 'RepeatThisPhrase'|get_plugin_lang('WhispeakAuthPlugin') }}</p>
<div class="well well-sm">
@ -15,51 +15,57 @@
</div>
{% endblock %}
<ul class="list-inline text-center">
<li>
<div class="text-center">
{% if action == 'enrollment' %}
<p>
<label>
<input type="checkbox" id="chk-agree-research">
{{ 'AgreeAllowResearch'|get_plugin_lang('WhispeakAuthPlugin') }}
</label>
</p>
{% endif %}
<p>
<button class="btn btn-primary" type="button" id="btn-start-record">
<span class="fa fa-circle fa-fw" aria-hidden="true"></span> {{ 'StartRecordingAudio'|get_lang }}
</button>
</li>
<li class="hidden">
<button class="btn btn-danger" type="button" id="btn-stop-record" disabled>
</p>
<p class="hidden">
<button class="btn btn-danger" type="button" id="btn-stop-record" disabled
data-loadingtext="{{ 'Uploading'|get_lang }}">
<span class="fa fa-square fa-fw" aria-hidden="true"></span> {{ 'StopRecordingAudio'|get_lang }}
</button>
</li>
<li class="hidden">
<button class="btn btn-success" type="button" id="btn-save-record"
data-loadingtext="{{ 'Uploading'|get_lang }}" disabled>
<span class="fa fa-send fa-fw" aria-hidden="true"></span> {{ 'SaveRecordedAudio'|get_lang }}
</button>
</li>
</ul>
<p class="hidden" id="audio-wrapper">
<audio class="center-block" controls id="record-preview"></audio>
</p>
</p>
</div>
</div>
<div class="col-sm-5 col-sm-offset-1" id="messages-deck">
{% if is_authenticated %}
<div class="alert alert-info">
<span class="fa fa-info-circle" aria-hidden="true"></span>
<strong>{{ 'SpeechAuthAlreadyEnrolled'|get_plugin_lang('WhispeakAuthPlugin') }}</strong>
</div>
{% endif %}
<div class="col-sm-8 col-sm-offset-2 col-md-5 col-md-offset-1">
<hr class="visible-sm">
<div id="messages-deck"></div>
<div class="hidden" id="audio-wrapper">
<p>
<audio class="center-block" controls id="record-preview"></audio>
</p>
</div>
</div>
</div>
<script>
$(function () {
{% block config_data %}
var data = {action: 'enrollment', license: 0 };
{% if action == 'enrollment' %}
$('#chk-agree-research').on('change', function () {
data.license = $('#chk-agree-research').is(':checked') ? 1 : 0;
});
{% endif %}
RecordAudio.init(
{
blockId: '#record-audio-recordrtc',
btnStartId: '#btn-start-record',
btnStopId: '#btn-stop-record',
btnSaveId: '#btn-save-record',
plyrPreviewId: '#record-preview',
data: {
action: 'enrollment'
}
data: data
}
);
{% endblock %}

Loading…
Cancel
Save