Bladeren bron

Plugin: Restoring Whispeakauth - refs BT#14921

Angel Fernando Quiroz Campos 5 jaren geleden
bovenliggende
commit
915fa57b4b

+ 7 - 0
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.

+ 293 - 0
plugin/whispeakauth/WhispeakAuthPlugin.php

@@ -0,0 +1,293 @@
+<?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 SETTING_INSTRUCTION = 'instruction';
+
+    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',
+                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->createQuery('DELETE FROM ChamiloCoreBundle:ExtraFieldValues efv WHERE efv.field = :field')
+            ->execute(['field' => $extraField]);
+
+        $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);
+    }
+}

+ 136 - 0
plugin/whispeakauth/ajax/record_audio.php

@@ -0,0 +1,136 @@
+<?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();
+
+$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';
+
+    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 = '<strong>'.$plugin->get_lang('EnrollmentSuccess').'</strong>';
+    $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 '<script>window.location.href = "'.api_get_path(WEB_PATH).'";</script>';
+
+    exit;
+}

+ 139 - 0
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);
+        }
+    }
+})();

+ 26 - 0
plugin/whispeakauth/authentify.php

@@ -0,0 +1,26 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+$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');
+
+$template->assign('header', $plugin->get_title());
+$template->assign('content', $content);
+$template->display_one_col_template();

+ 28 - 0
plugin/whispeakauth/enrollment.php

@@ -0,0 +1,28 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+$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');
+
+$template->assign('header', $plugin->get_title());
+$template->assign('content', $content);
+$template->display_one_col_template();

+ 14 - 0
plugin/whispeakauth/index.php

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

+ 4 - 0
plugin/whispeakauth/install.php

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

+ 33 - 0
plugin/whispeakauth/lang/english.php

@@ -0,0 +1,33 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+$strings['plugin_title'] = 'Speech authentication with Whispeak';
+$strings['plugin_comment'] = 'Allow speech authentication in Chamilo.';
+
+$strings['enable'] = 'Enable';
+$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['EnrollmentSampleText'] = 'The famous Mona Lisa painting was painted by Leonardo Da Vinci.';
+$strings['AuthentifySampleText1'] = 'Dropping Like Flies.';
+$strings['AuthentifySampleText2'] = 'Keep Your Eyes Peeled.';
+$strings['AuthentifySampleText3'] = 'The fox screams at midnight.';
+$strings['AuthentifySampleText4'] = 'Go Out On a Limb.';
+$strings['AuthentifySampleText5'] = 'Under the Water.';
+$strings['AuthentifySampleText6'] = 'Barking Up The Wrong Tree.';
+
+$strings['RepeatThisPhrase'] = 'Repeat this phrase three times after allowing audio recording:';
+$strings['EnrollmentSignature0'] = 'Unsustainable signature requires a new enrollment.';
+$strings['EnrollmentSignature1'] = 'Passable signature, advice to make a new enrollment.';
+$strings['EnrollmentSignature2'] = 'Correct signature.';
+$strings['EnrollmentSignature3'] = 'Good signature.';
+$strings['SpeechAuthAlreadyEnrolled'] = 'Speech authentication already 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';

+ 18 - 0
plugin/whispeakauth/lang/french.php

@@ -0,0 +1,18 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+$strings['plugin_title'] = 'Authentification vocale avec Whispeak';
+
+$strings['EnrollmentSampleText'] = 'Le fameux chef-d\'oeuvre Mona Lisa a été peint par Léonardo da Vinci.';
+
+$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['EnrollmentSignature2'] = 'Signature correcte.';
+$strings['EnrollmentSignature3'] = 'Signature bonne.';
+$strings['SpeechAuthentication'] = 'Authentification de 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';

+ 33 - 0
plugin/whispeakauth/lang/spanish.php

@@ -0,0 +1,33 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+$strings['plugin_title'] = 'Authenticación de voz con Whispeak';
+$strings['plugin_comment'] = 'Permitir autenticación de voz en Chamilo.';
+
+$strings['enable'] = 'Habilitar';
+$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['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.';

+ 4 - 0
plugin/whispeakauth/plugin.php

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

+ 4 - 0
plugin/whispeakauth/uninstall.php

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

+ 38 - 0
plugin/whispeakauth/view/authentify_recorder.html.twig

@@ -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 %}

+ 67 - 0
plugin/whispeakauth/view/record_audio.html.twig

@@ -0,0 +1,67 @@
+<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">
+        {% 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>
+</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>