diff --git a/plugin/whispeakauth/WhispeakAuthPlugin.php b/plugin/whispeakauth/WhispeakAuthPlugin.php
new file mode 100644
index 0000000000..75e147e462
--- /dev/null
+++ b/plugin/whispeakauth/WhispeakAuthPlugin.php
@@ -0,0 +1,256 @@
+ 'boolean',
+ self::SETTING_API_URL => 'text',
+ self::SETTING_TOKEN => 'text',
+ '
Add $_configuration[\'whispeak_auth_enabled\'] = true;
'.
+ 'in configuration.php
file
' => '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';
+ }
+
+ /**
+ * @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);
+ }
+
+ /**
+ * @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
+ );
+ }
+
+ public function getAuthentifySampleText()
+ {
+ return 'Hola hola hola';
+ }
+}
diff --git a/plugin/whispeakauth/ajax/record_audio.php b/plugin/whispeakauth/ajax/record_audio.php
new file mode 100644
index 0000000000..c6b2d620e2
--- /dev/null
+++ b/plugin/whispeakauth/ajax/record_audio.php
@@ -0,0 +1,118 @@
+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'];
+$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;
+ }
+
+ $plugin->saveEnrollment($user, $result['uid']);
+
+ echo Display::return_message($plugin->get_lang('EnrollmentSuccess'), 'success');
+
+ exit;
+}
+
+if ($isAuthentify) {
+ $result = $plugin->requestAuthentify($user, $newFullPath);
+
+ if (empty($result)) {
+ echo Display::return_message($plugin->get_lang('AuthentifyFailed'));
+
+ exit;
+ }
+
+ $success = (bool) $result['audio'][0]['result'];
+
+ if (!$success) {
+ echo Display::return_message($plugin->get_lang('TryAgain'));
+
+ 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..03df38b376
--- /dev/null
+++ b/plugin/whispeakauth/assets/js/RecordAudio.js
@@ -0,0 +1,138 @@
+/* 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, {
+ 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..f2d54a05b7
--- /dev/null
+++ b/plugin/whispeakauth/authentify.php
@@ -0,0 +1,24 @@
+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..f89a68baf5
--- /dev/null
+++ b/plugin/whispeakauth/enrollment.php
@@ -0,0 +1,24 @@
+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('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..2bfff31ba8
--- /dev/null
+++ b/plugin/whispeakauth/index.php
@@ -0,0 +1,12 @@
+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..2ecf56f488
--- /dev/null
+++ b/plugin/whispeakauth/lang/english.php
@@ -0,0 +1,15 @@
+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 %}
+
+
+
+
+ {{ parent() }}
+{% endblock %}
+
+{% block config_data %}
+ $('#username').on('change', function () {
+ $('#record-audio-recordrtc, #btn-start-record, #btn-stop-record, #btn-save-record').off('click', '');
+
+ RecordAudio.init(
+ {
+ blockId: '#record-audio-recordrtc',
+ btnStartId: '#btn-start-record',
+ btnStopId: '#btn-stop-record',
+ btnSaveId: '#btn-save-record',
+ plyrPreviewId: '#record-preview',
+ data: {
+ action: 'authentify',
+ username: $('#username').val()
+ }
+ }
+ );
+ });
+{% endblock %}
diff --git a/plugin/whispeakauth/view/record_audio.html.twig b/plugin/whispeakauth/view/record_audio.html.twig
new file mode 100644
index 0000000000..7a67132f91
--- /dev/null
+++ b/plugin/whispeakauth/view/record_audio.html.twig
@@ -0,0 +1,60 @@
+
+
+ {% block intro %}
+
{{ 'RepeatThisPhrase'|get_plugin_lang('WhispeakAuthPlugin') }}
+
+
+
+
+ {{ 'RecordAudio'|get_lang }}
+
+
+
+
+ {% endblock %}
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+