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 }} +
+
+

{{ sample_text }}

+
+
+
+ {% endblock %} + + + +
+
+
+ +