Create whispeaker auth plugin - refs BT#14921

pull/2715/head
Angel Fernando Quiroz Campos 7 years ago
parent 436307390b
commit 9bae181917
  1. 256
      plugin/whispeakauth/WhispeakAuthPlugin.php
  2. 118
      plugin/whispeakauth/ajax/record_audio.php
  3. 138
      plugin/whispeakauth/assets/js/RecordAudio.js
  4. 24
      plugin/whispeakauth/authentify.php
  5. 24
      plugin/whispeakauth/enrollment.php
  6. 12
      plugin/whispeakauth/index.php
  7. 4
      plugin/whispeakauth/install.php
  8. 15
      plugin/whispeakauth/lang/english.php
  9. 8
      plugin/whispeakauth/lang/spanish.php
  10. 4
      plugin/whispeakauth/plugin.php
  11. 4
      plugin/whispeakauth/uninstall.php
  12. 38
      plugin/whispeakauth/view/authentify_recorder.html.twig
  13. 60
      plugin/whispeakauth/view/record_audio.html.twig

@ -0,0 +1,256 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\ExtraField;
use Chamilo\CoreBundle\Entity\ExtraFieldValues;
use Chamilo\UserBundle\Entity\User;
/**
* Class WhispeakAuthPlugin.
*/
class WhispeakAuthPlugin extends Plugin
{
const SETTING_ENABLE = 'enable';
const SETTING_API_URL = 'api_url';
const SETTING_TOKEN = 'token';
const EXTRAFIELD_AUTH_UID = 'whispeak_auth_uid';
/**
* StudentFollowUpPlugin constructor.
*/
protected function __construct()
{
parent::__construct(
'0.1',
'Angel Fernando Quiroz',
[
self::SETTING_ENABLE => 'boolean',
self::SETTING_API_URL => 'text',
self::SETTING_TOKEN => 'text',
'<p>Add <code>$_configuration[\'whispeak_auth_enabled\'] = true;</code> '.
'in <code>configuration.php</code> file</p>' => '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';
}
}

@ -0,0 +1,118 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\UserBundle\Entity\User;
use FFMpeg\FFMpeg;
use FFMpeg\Format\Audio\Wav;
$cidReset = true;
require_once __DIR__.'/../../../main/inc/global.inc.php';
$action = isset($_POST['action']) ? $_POST['action'] : 'enrollment';
$isEnrollment = 'enrollment' === $action;
$isAuthentify = 'authentify' === $action;
$isAllowed = true;
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();
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'];
$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 '<script>window.location.href = "'.api_get_path(WEB_PATH).'";</script>';
exit;
}

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

@ -0,0 +1,24 @@
<?php
/* For licensing terms, see /license.txt */
$cidReset = true;
require_once __DIR__.'/../../main/inc/global.inc.php';
$plugin = WhispeakAuthPlugin::create();
$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();

@ -0,0 +1,24 @@
<?php
/* For licensing terms, see /license.txt */
$cidReset = true;
require_once __DIR__.'/../../main/inc/global.inc.php';
api_block_anonymous_users(true);
$plugin = WhispeakAuthPlugin::create();
$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('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();

@ -0,0 +1,12 @@
<?php
/* For licensing terms, see /license.txt */
$plugin = WhispeakAuthPlugin::create();
echo Display::toolbarButton(
$plugin->get_lang('SpeechAuthentication'),
api_get_path(WEB_PLUGIN_PATH).'whispeakauth/authentify.php',
'sign-in',
'info',
['class' => 'btn-block']
);

@ -0,0 +1,4 @@
<?php
/* For licensing terms, see /license.txt */
WhispeakAuthPlugin::create()->install();

@ -0,0 +1,15 @@
<?php
/* For licensing terms, see /license.txt */
$strings['plugin_title'] = 'Speech authentication with Whispeak';
$strings['EnrollmentSampleText'] = 'The famous Mona Lisa painting was painted by Leonardo Da Vinci.';
$strings['RepeatThisPhrase'] = 'Repeat this phrase three times after allowing audio recording:';
$strings['SpeechAuthentication'] = 'Speech authentication';
$strings['EnrollmentFailed'] = 'Enrollment failed.';
$strings['EnrollmentSuccess'] = 'Enrollment success.';
$strings['AuthentifyFailed'] = 'Login failed.';
$strings['AuthentifySuccess'] = '¡Authentication success!';
$strings['TryAgain'] = 'Try again';

@ -0,0 +1,8 @@
<?php
/* For licensing terms, see /license.txt */
$strings['plugin_title'] = 'Authenticación de voz con Whispeak';
$strings['EnrollmentSampleText'] = 'El famoso cuadro de Mona Lisa fue pintado por Leonardo Da Vinci.';
$strings['RepeatThisPhrase'] = 'Repita esta frase tres veces después de permitir la grabación de audio:';

@ -0,0 +1,4 @@
<?php
/* For licensing terms, see /license.txt */
$plugin_info = WhispeakAuthPlugin::create()->get_info();

@ -0,0 +1,4 @@
<?php
/* For licensing terms, see /license.txt */
WhispeakAuthPlugin::create()->uninstall();

@ -0,0 +1,38 @@
{% 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">
</div>
</div>
</form>
<hr>
{{ 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 %}

@ -0,0 +1,60 @@
<div id="record-audio-recordrtc" class="row">
<div class="col-sm-6">
{% block intro %}
<p class="text-center">{{ 'RepeatThisPhrase'|get_plugin_lang('WhispeakAuthPlugin') }}</p>
<div class="well well-sm">
<div class="row">
<div class="col-sm-3 text-center">
<span class="fa fa-microphone fa-5x fa-fw" aria-hidden="true"></span>
<span class="sr-only">{{ 'RecordAudio'|get_lang }}</span>
</div>
<div class="col-sm-9 text-center">
<p class="lead">{{ sample_text }}</p>
</div>
</div>
</div>
{% endblock %}
<ul class="list-inline text-center">
<li>
<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>
<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>
</div>
<div class="col-sm-5 col-sm-offset-1" id="messages-deck"></div>
</div>
<script>
$(document).on('ready', function () {
{% block config_data %}
RecordAudio.init(
{
blockId: '#record-audio-recordrtc',
btnStartId: '#btn-start-record',
btnStopId: '#btn-stop-record',
btnSaveId: '#btn-save-record',
plyrPreviewId: '#record-preview',
data: {
action: 'enrollment'
}
}
);
{% endblock %}
});
</script>
Loading…
Cancel
Save