diff --git a/main/inc/lib/userportal.lib.php b/main/inc/lib/userportal.lib.php
index 03e4600a58..9cde0316d9 100755
--- a/main/inc/lib/userportal.lib.php
+++ b/main/inc/lib/userportal.lib.php
@@ -946,6 +946,19 @@ class IndexManager
];
}
+ if (true === api_get_configuration_value('whispeak_auth_enabled')) {
+ //if (!WhispeakAuthPlugin::checkUserIsEnrolled($userId)) {
+ $itemTitle = WhispeakAuthPlugin::create()->get_title();
+
+ $items[] = [
+ 'class' => 'whispeak-enrollment',
+ 'icon' => Display::return_icon('addworkuser.png', $itemTitle),
+ 'link' => WhispeakAuthPlugin::getEnrollmentUrl(),
+ 'title' => $itemTitle,
+ ];
+ //}
+ }
+
return $items;
}
diff --git a/plugin/whispeakauth/README.md b/plugin/whispeakauth/README.md
new file mode 100644
index 0000000000..2031255d3d
--- /dev/null
+++ b/plugin/whispeakauth/README.md
@@ -0,0 +1,7 @@
+# Speech authentication with Whispeak
+
+Instructions:
+1. Install plugin in Chamilo.
+2. Set the plugin configuration with the token and API url. And enable the plugin.
+3. Set the `login_bottom` region to the plugin.
+4. Add `$_configuration['whispeak_auth_enabled'] = true;` to `configuration.php` file.
diff --git a/plugin/whispeakauth/WhispeakAuthPlugin.php b/plugin/whispeakauth/WhispeakAuthPlugin.php
new file mode 100644
index 0000000000..81582d2dc2
--- /dev/null
+++ b/plugin/whispeakauth/WhispeakAuthPlugin.php
@@ -0,0 +1,289 @@
+ 'boolean',
+ self::SETTING_API_URL => 'text',
+ self::SETTING_TOKEN => 'text',
+ self::SETTING_INSTRUCTION => 'html',
+ ]
+ );
+ }
+
+ /**
+ * @return WhispeakAuthPlugin
+ */
+ public static function create()
+ {
+ static $result = null;
+
+ return $result ? $result : $result = new self();
+ }
+
+ public function install()
+ {
+ UserManager::create_extra_field(
+ self::EXTRAFIELD_AUTH_UID,
+ \ExtraField::FIELD_TYPE_TEXT,
+ $this->get_lang('Whispeak uid'),
+ ''
+ );
+ }
+
+ public function uninstall()
+ {
+ $extraField = self::getAuthUidExtraField();
+
+ if (empty($extraField)) {
+ return;
+ }
+
+ $em = Database::getManager();
+ $em->remove($extraField);
+ $em->flush();
+ }
+
+ /**
+ * @return ExtraField
+ */
+ public static function getAuthUidExtraField()
+ {
+ $em = Database::getManager();
+ $efRepo = $em->getRepository('ChamiloCoreBundle:ExtraField');
+
+ /** @var ExtraField $extraField */
+ $extraField = $efRepo->findOneBy(
+ [
+ 'variable' => self::EXTRAFIELD_AUTH_UID,
+ 'extraFieldType' => ExtraField::USER_FIELD_TYPE,
+ ]
+ );
+
+ return $extraField;
+ }
+
+ /**
+ * @param int $userId
+ *
+ * @return ExtraFieldValues
+ */
+ public static function getAuthUidValue($userId)
+ {
+ $extraField = self::getAuthUidExtraField();
+ $em = Database::getManager();
+ $efvRepo = $em->getRepository('ChamiloCoreBundle:ExtraFieldValues');
+
+ /** @var ExtraFieldValues $value */
+ $value = $efvRepo->findOneBy(['field' => $extraField, 'itemId' => $userId]);
+
+ return $value;
+ }
+
+ /**
+ * @param int $userId
+ *
+ * @return bool
+ */
+ public static function checkUserIsEnrolled($userId)
+ {
+ $value = self::getAuthUidValue($userId);
+
+ if (empty($value)) {
+ return false;
+ }
+
+ return !empty($value->getValue());
+ }
+
+ /**
+ * @return string
+ */
+ public static function getEnrollmentUrl()
+ {
+ return api_get_path(WEB_PLUGIN_PATH).'whispeakauth/enrollment.php';
+ }
+
+ /**
+ * @param User $user
+ * @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
+ */
+ public function saveEnrollment(User $user, $uid)
+ {
+ $em = Database::getManager();
+ $value = self::getAuthUidValue($user->getId());
+
+ if (empty($value)) {
+ $ef = self::getAuthUidExtraField();
+ $now = new DateTime('now', new DateTimeZone('UTC'));
+
+ $value = new ExtraFieldValues();
+ $value
+ ->setField($ef)
+ ->setItemId($user->getId())
+ ->setUpdatedAt($now);
+ }
+
+ $value->setValue($uid);
+
+ $em->persist($value);
+ $em->flush();
+ }
+
+ public function requestAuthentify(User $user, $filePath)
+ {
+ $value = self::getAuthUidValue($user->getId());
+
+ if (empty($value)) {
+ return null;
+ }
+
+ $metadata = [
+ 'uid' => $value->getValue(),
+ 'audioType' => 'pcm',
+ ];
+
+ return $this->sendRequest(
+ 'authentify',
+ $metadata,
+ $user,
+ $filePath
+ );
+ }
+
+ /**
+ * @return string
+ */
+ public function getAuthentifySampleText()
+ {
+ $phrases = [];
+
+ for ($i = 1; $i <= 6; $i++) {
+ $phrases[] = $this->get_lang("AuthentifySampleText$i");
+ }
+
+ $rand = array_rand($phrases, 1);
+
+ return $phrases[$rand];
+ }
+
+ /**
+ * @return bool
+ */
+ public function toolIsEnabled()
+ {
+ return 'true' === $this->get(self::SETTING_ENABLE);
+ }
+
+ /**
+ * Access not allowed when tool is not enabled.
+ *
+ * @param bool $printHeaders Optional. Print headers.
+ */
+ public function protectTool($printHeaders = true)
+ {
+ if ($this->toolIsEnabled()) {
+ return;
+ }
+
+ api_not_allowed($printHeaders);
+ }
+
+ /**
+ * @return string
+ */
+ private function getApiUrl()
+ {
+ $url = $this->get(self::SETTING_API_URL);
+
+ return trim($url, " \t\n\r \v/");
+ }
+
+ /**
+ * @param string $endPoint
+ * @param array $metadata
+ * @param User $user
+ * @param string $filePath
+ *
+ * @return array
+ */
+ private function sendRequest($endPoint, array $metadata, User $user, $filePath)
+ {
+ $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),
+ ];
+
+ $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);
+
+ $result = json_decode($result, true);
+
+ if (!empty($result['error'])) {
+ return null;
+ }
+
+ return json_decode($result, true);
+ }
+}
diff --git a/plugin/whispeakauth/ajax/record_audio.php b/plugin/whispeakauth/ajax/record_audio.php
new file mode 100644
index 0000000000..6c20a632e8
--- /dev/null
+++ b/plugin/whispeakauth/ajax/record_audio.php
@@ -0,0 +1,136 @@
+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;
+}
+
+$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';
+
+ try {
+ $ffmpeg = FFMpeg::create();
+
+ $audio = $ffmpeg->open($originFullPath);
+ $audio->save(new Wav(), $newFullPath);
+ } catch (Exception $exception) {
+ echo Display::return_message($exception->getMessage(), 'error');
+
+ exit;
+ }
+}
+
+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 = ''.$plugin->get_lang('EnrollmentSuccess').'';
+ $message .= PHP_EOL;
+ $message .= $plugin->get_lang("EnrollmentSignature$reliability");
+
+ echo Display::return_message($message, 'success', false);
+
+ exit;
+}
+
+if ($isAuthentify) {
+ $result = $plugin->requestAuthentify($user, $newFullPath);
+
+ 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;
+ }
+
+ $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 '';
+
+ exit;
+}
diff --git a/plugin/whispeakauth/assets/js/RecordAudio.js b/plugin/whispeakauth/assets/js/RecordAudio.js
new file mode 100644
index 0000000000..a221a00df5
--- /dev/null
+++ b/plugin/whispeakauth/assets/js/RecordAudio.js
@@ -0,0 +1,139 @@
+/* For licensing terms, see /license.txt */
+
+window.RecordAudio = (function () {
+ function useRecordRTC(rtcInfo) {
+ $(rtcInfo.blockId).show();
+
+ var mediaConstraints = {audio: true},
+ localStream = null,
+ recordRTC = null,
+ btnStart = $(rtcInfo.btnStartId),
+ btnStop = $(rtcInfo.btnStopId),
+ btnSave = $(rtcInfo.btnSaveId),
+ tagAudio = $(rtcInfo.plyrPreviewId);
+
+ function saveAudio() {
+ var recordedBlob = recordRTC.getBlob();
+
+ if (!recordedBlob) {
+ return;
+ }
+
+ var btnSaveText = btnSave.html();
+ var fileExtension = recordedBlob.type.split('/')[1];
+
+ var formData = new FormData();
+ formData.append('audio', recordedBlob, 'audio.' + fileExtension);
+
+ for (var prop in rtcInfo.data) {
+ if (!rtcInfo.data.hasOwnProperty(prop)) {
+ continue;
+ }
+
+ formData.append(prop, rtcInfo.data[prop]);
+ }
+
+ $.ajax({
+ url: _p.web_plugin + 'whispeakauth/ajax/record_audio.php',
+ data: formData,
+ processData: false,
+ contentType: false,
+ type: 'POST',
+ beforeSend: function () {
+ btnStart.prop('disabled', true);
+ btnStop.prop('disabled', true);
+ btnSave.prop('disabled', true).text(btnSave.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');
+ });
+ }
+
+ btnStart.on('click', function () {
+ tagAudio.prop('src', '');
+
+ function successCallback(stream) {
+ localStream = stream;
+
+ recordRTC = RecordRTC(stream, {
+ recorderType: StereoAudioRecorder,
+ numberOfAudioChannels: 1,
+ type: 'audio'
+ });
+ 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');
+ }
+
+ function errorCallback(error) {
+ alert(error.message);
+ }
+
+ if (navigator.mediaDevices.getUserMedia) {
+ navigator.mediaDevices.getUserMedia(mediaConstraints)
+ .then(successCallback)
+ .catch(errorCallback);
+
+ return;
+ }
+
+ navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia;
+
+ if (navigator.getUserMedia) {
+ navigator.getUserMedia(mediaConstraints, successCallback, errorCallback);
+ }
+ });
+
+ btnStop.on('click', function () {
+ if (!recordRTC) {
+ return;
+ }
+
+ 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');
+
+ localStream.getTracks()[0].stop();
+ });
+ });
+
+ btnSave.on('click', function () {
+ if (!recordRTC) {
+ return;
+ }
+
+ saveAudio();
+ });
+ }
+
+ return {
+ init: function (rtcInfo) {
+ $(rtcInfo.blockId).hide();
+
+ var userMediaEnabled = (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) ||
+ !!navigator.webkitGetUserMedia ||
+ !!navigator.mozGetUserMedia ||
+ !!navigator.getUserMedia;
+
+ if (!userMediaEnabled) {
+ return;
+ }
+
+ useRecordRTC(rtcInfo);
+ }
+ }
+})();
\ No newline at end of file
diff --git a/plugin/whispeakauth/authentify.php b/plugin/whispeakauth/authentify.php
new file mode 100644
index 0000000000..f63ac7bef3
--- /dev/null
+++ b/plugin/whispeakauth/authentify.php
@@ -0,0 +1,26 @@
+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');
+
+$template->assign('header', $plugin->get_title());
+$template->assign('content', $content);
+$template->display_one_col_template();
diff --git a/plugin/whispeakauth/enrollment.php b/plugin/whispeakauth/enrollment.php
new file mode 100644
index 0000000000..9ea8192e11
--- /dev/null
+++ b/plugin/whispeakauth/enrollment.php
@@ -0,0 +1,28 @@
+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');
+
+$template->assign('header', $plugin->get_title());
+$template->assign('content', $content);
+$template->display_one_col_template();
diff --git a/plugin/whispeakauth/index.php b/plugin/whispeakauth/index.php
new file mode 100644
index 0000000000..420d242d3d
--- /dev/null
+++ b/plugin/whispeakauth/index.php
@@ -0,0 +1,14 @@
+toolIsEnabled()) {
+ echo Display::toolbarButton(
+ $plugin->get_lang('SpeechAuthentication'),
+ api_get_path(WEB_PLUGIN_PATH).'whispeakauth/authentify.php',
+ 'sign-in',
+ 'info',
+ ['class' => 'btn-block']
+ );
+}
diff --git a/plugin/whispeakauth/install.php b/plugin/whispeakauth/install.php
new file mode 100644
index 0000000000..adf5155af7
--- /dev/null
+++ b/plugin/whispeakauth/install.php
@@ -0,0 +1,4 @@
+install();
diff --git a/plugin/whispeakauth/lang/english.php b/plugin/whispeakauth/lang/english.php
new file mode 100644
index 0000000000..2fb49f9f3f
--- /dev/null
+++ b/plugin/whispeakauth/lang/english.php
@@ -0,0 +1,33 @@
+Add $_configuration[\'whispeak_auth_enabled\'] = true;
'.
+ 'in the configuration.php
file
$_configuration[\'whispeak_auth_enabled\'] = true;
'.
+ 'al archivo configuration.php
';
+
+$strings['EnrollmentSampleText'] = 'El famoso cuadro de Mona Lisa fue pintado por Leonardo Da Vinci.';
+$strings['AuthentifySampleText1'] = 'Cayendo como moscas.';
+$strings['AuthentifySampleText2'] = 'Mantén tus ojos abiertos.';
+$strings['AuthentifySampleText3'] = 'El zorro grita a medianoche.';
+$strings['AuthentifySampleText4'] = 'Ir por las ramas.';
+$strings['AuthentifySampleText5'] = 'Debajo del agua.';
+$strings['AuthentifySampleText6'] = 'Ladrando al árbol equivocado.';
+
+$strings['RepeatThisPhrase'] = 'Repita esta frase tres veces después de permitir la grabación de audio:';
+$strings['EnrollmentSignature0'] = 'Firma insostenible, requiere una nueva inscripción.';
+$strings['EnrollmentSignature1'] = 'Firma aceptable, pero se aconseja hacer una nueva inscripción.';
+$strings['EnrollmentSignature2'] = 'Firma correcta.';
+$strings['EnrollmentSignature3'] = 'Buena firma.';
+$strings['SpeechAuthAlreadyEnrolled'] = 'Autenticación de voz registrada anteriormente.';
+$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.';
diff --git a/plugin/whispeakauth/plugin.php b/plugin/whispeakauth/plugin.php
new file mode 100644
index 0000000000..c990a9158d
--- /dev/null
+++ b/plugin/whispeakauth/plugin.php
@@ -0,0 +1,4 @@
+get_info();
diff --git a/plugin/whispeakauth/uninstall.php b/plugin/whispeakauth/uninstall.php
new file mode 100644
index 0000000000..4f811df4d3
--- /dev/null
+++ b/plugin/whispeakauth/uninstall.php
@@ -0,0 +1,4 @@
+uninstall();
diff --git a/plugin/whispeakauth/view/authentify_recorder.html.twig b/plugin/whispeakauth/view/authentify_recorder.html.twig
new file mode 100644
index 0000000000..3c26b389e0
--- /dev/null
+++ b/plugin/whispeakauth/view/authentify_recorder.html.twig
@@ -0,0 +1,38 @@
+{% extends 'whispeakauth/view/record_audio.html.twig' %}
+
+{% block intro %}
+
+
+ {{ 'RepeatThisPhrase'|get_plugin_lang('WhispeakAuthPlugin') }}
+{{ sample_text }}
++ +
+