This commit is contained in:
Xes
2025-08-14 22:39:38 +02:00
parent 3641e93527
commit 5403f346e3
3370 changed files with 327179 additions and 0 deletions

View File

@@ -0,0 +1,366 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\WhispeakAuth\Controller;
use Chamilo\PluginBundle\Entity\WhispeakAuth\LogEvent;
use Chamilo\PluginBundle\WhispeakAuth\Request\ApiRequest;
use Chamilo\UserBundle\Entity\User;
use ChamiloSession;
use Display;
use Login;
use WhispeakAuthPlugin;
/**
* Class AuthenticationController.
*
* @package Chamilo\PluginBundle\WhispeakAuth\Controller
*/
class AuthenticationController extends BaseController
{
/**
* @throws \Exception
*/
public function index()
{
if (!$this->plugin->toolIsEnabled()) {
throw new \Exception(get_lang('NotAllowed'));
}
/** @var array $lpQuestionInfo */
$lpQuestionInfo = ChamiloSession::read(WhispeakAuthPlugin::SESSION_QUIZ_QUESTION, []);
if (ChamiloSession::read(WhispeakAuthPlugin::SESSION_AUTH_PASSWORD, false)) {
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_AUTH_PASSWORD);
if (empty($lpQuestionInfo)) {
$message = $this->plugin->get_lang('MaxAttemptsReached')
.'<br><strong>'.$this->plugin->get_lang('LoginWithUsernameAndPassword').'</strong>';
Display::addFlash(
Display::return_message($message, 'warning')
);
}
header('Location: '.api_get_path(WEB_PLUGIN_PATH).'whispeakauth/authentify_password.php');
exit;
}
/** @var array $lpItemInfo */
$lpItemInfo = ChamiloSession::read(WhispeakAuthPlugin::SESSION_LP_ITEM, []);
/** @var \learnpath $oLp */
$oLp = ChamiloSession::read('oLP', null);
/** @var \Exercise $objExercise */
$objExercise = ChamiloSession::read('objExercise', null);
$isAuthOnLp = !empty($lpItemInfo) && !empty($oLp);
$isAuthOnQuiz = !empty($lpQuestionInfo) && !empty($objExercise);
$showFullPage = !$isAuthOnLp && !$isAuthOnQuiz;
$user = api_get_user_entity(
ChamiloSession::read(WhispeakAuthPlugin::SESSION_2FA_USER, 0) ?: api_get_user_id()
);
$showForm = !$user;
if ($user) {
if (!WhispeakAuthPlugin::getAuthUidValue($user)) {
$message = Display::return_message($this->plugin->get_lang('SpeechAuthNotEnrolled'), 'warning');
if (!empty($lpQuestionInfo) && empty($lpItemInfo)) {
echo $message;
} else {
Display::addFlash($message);
}
header('Location: '.api_get_path(WEB_PLUGIN_PATH).'whispeakauth/authentify_password.php');
exit;
}
}
if (!empty($lpQuestionInfo) && empty($lpItemInfo)) {
echo api_get_js('rtc/RecordRTC.js');
echo api_get_js_simple(api_get_path(WEB_PLUGIN_PATH).'whispeakauth/assets/js/RecordAudio.js');
}
$request = new ApiRequest();
$response = $request->createAuthenticationSessionToken($user);
if (empty($response['text'])) {
$varNumber = mt_rand(1, 6);
$response['text'] = $this->plugin->get_lang("AuthentifySampleText$varNumber");
}
ChamiloSession::write(WhispeakAuthPlugin::SESSION_SENTENCE_TEXT, $response['token']);
if (!empty($lpQuestionInfo) && empty($lpItemInfo)) {
$template = new \Template('', $showFullPage, $showFullPage, false, true, false);
$template->assign('show_form', $showForm);
$template->assign('sample_text', $response['text']);
echo $template->fetch('whispeakauth/view/authentify_recorder.html.twig');
exit;
}
$this->displayPage(
$showFullPage,
[
'show_form' => $showForm,
'sample_text' => $response['text'],
]
);
}
/**
* @throws \Exception
*/
public function ajax()
{
$userId = api_get_user_id();
$user2fa = ChamiloSession::read(WhispeakAuthPlugin::SESSION_2FA_USER, 0);
$result = [];
if (!empty($user2fa) || !empty($userId)) {
$isAllowed = !empty($_FILES['audio']);
} else {
$isAllowed = !empty($_POST['username']) && !empty($_FILES['audio']);
}
if (!$isAllowed || !$this->plugin->toolIsEnabled()) {
throw new \Exception(get_lang('NotAllowed'));
}
if (!empty($user2fa)) {
$user = api_get_user_entity($user2fa);
} elseif (!empty($userId)) {
$user = api_get_user_entity($userId);
} else {
/** @var User|null $user */
$user = \UserManager::getRepository()->findOneBy(['username' => $_POST['username']]);
}
if (!$user) {
throw new \Exception(get_lang('NotFound'));
}
$audioFilePath = $this->uploadAudioFile($user);
$failedLogins = ChamiloSession::read(WhispeakAuthPlugin::SESSION_FAILED_LOGINS, 0);
$maxAttempts = $this->plugin->getMaxAttempts();
if ($maxAttempts && $failedLogins >= $maxAttempts) {
throw new \Exception($this->plugin->get_lang('MaxAttemptsReached'));
}
$token = \ChamiloSession::read(\WhispeakAuthPlugin::SESSION_SENTENCE_TEXT);
\ChamiloSession::erase(\WhispeakAuthPlugin::SESSION_SENTENCE_TEXT);
/** @var array $lpItemInfo */
$lpItemInfo = ChamiloSession::read(WhispeakAuthPlugin::SESSION_LP_ITEM, []);
/** @var array $quizQuestionInfo */
$quizQuestionInfo = ChamiloSession::read(WhispeakAuthPlugin::SESSION_QUIZ_QUESTION, []);
$success = true;
$request = new ApiRequest();
try {
$request->performAuthentication($token, $user, $audioFilePath);
$message = $this->plugin->get_lang('AuthentifySuccess');
} catch (\Exception $exception) {
$message = $this->plugin->get_lang('AuthentifyFailed')
.PHP_EOL
.$exception->getMessage();
$success = false;
}
if (!$success) {
if (!empty($lpItemInfo)) {
$this->plugin->addAttemptInLearningPath(
LogEvent::STATUS_FAILED,
$user->getId(),
$lpItemInfo['lp_item'],
$lpItemInfo['lp']
);
}
if (!empty($quizQuestionInfo)) {
$this->plugin->addAttemptInQuiz(
LogEvent::STATUS_FAILED,
$user->getId(),
$quizQuestionInfo['question'],
$quizQuestionInfo['quiz']
);
}
if (empty($lpItemInfo) && empty($quizQuestionInfo)) {
$this->plugin->addAuthenticationAttempt(LogEvent::STATUS_FAILED, $user->getId());
}
$authTokenRequest = new ApiRequest();
$authTokenResponse = $authTokenRequest->createAuthenticationSessionToken($user);
if (empty($authTokenResponse['text'])) {
$varNumber = mt_rand(1, 6);
$authTokenResponse['text'] = $this->plugin->get_lang("AuthentifySampleText$varNumber");
}
$result['text'] = $authTokenResponse['text'];
ChamiloSession::write(WhispeakAuthPlugin::SESSION_SENTENCE_TEXT, $authTokenResponse['token']);
ChamiloSession::write(WhispeakAuthPlugin::SESSION_FAILED_LOGINS, ++$failedLogins);
if ($maxAttempts && $failedLogins >= $maxAttempts) {
$message .= PHP_EOL
.'<span data-reach-attempts="true">'.$this->plugin->get_lang('MaxAttemptsReached').'</span>'
.PHP_EOL.PHP_EOL
.'<strong>'
.$this->plugin->get_lang('LoginWithUsernameAndPassword')
.'</strong>';
if (!empty($user2fa)) {
Display::addFlash(
Display::return_message($message, 'warning', false)
);
}
} else {
$message .= PHP_EOL.$this->plugin->get_lang('TryAgain');
if ('true' === api_get_setting('allow_lostpassword')) {
$message .= PHP_EOL
.Display::url(
get_lang('LostPassword'),
api_get_path(WEB_CODE_PATH).'auth/lostPassword.php',
['target' => $lpItemInfo ? '_top' : '_self']
);
}
}
}
$result['resultHtml'] = Display::return_message(
nl2br($message),
$success ? 'success' : 'warning',
false
);
if (!$success && $maxAttempts && $failedLogins >= $maxAttempts) {
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_FAILED_LOGINS);
if (!empty($lpItemInfo)) {
$result['resultHtml'] .= '<script>window.location.href = "'
.api_get_path(WEB_PLUGIN_PATH)
.'whispeakauth/authentify_password.php";</script>';
return $result;
}
if (!empty($quizQuestionInfo)) {
$url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit.php?'.$quizQuestionInfo['url_params'];
ChamiloSession::write(WhispeakAuthPlugin::SESSION_AUTH_PASSWORD, true);
$result['resultHtml'] .= "<script>window.location.href = '".$url."';</script>";
return $result;
}
$result['resultHtml'] .= '<script>window.location.href = "'.api_get_path(WEB_PATH).'";</script>';
return $result;
}
if ($success) {
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_SENTENCE_TEXT);
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_FAILED_LOGINS);
if (!empty($lpItemInfo)) {
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_LP_ITEM);
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_2FA_USER);
$this->plugin->addAttemptInLearningPath(
LogEvent::STATUS_SUCCESS,
$user->getId(),
$lpItemInfo['lp_item'],
$lpItemInfo['lp']
);
$result['resultHtml'] .= '<script>window.location.href = "'.$lpItemInfo['src'].'";</script>';
return $result;
}
if (!empty($quizQuestionInfo)) {
$quizQuestionInfo['passed'] = true;
$url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit.php?'.$quizQuestionInfo['url_params'];
ChamiloSession::write(WhispeakAuthPlugin::SESSION_QUIZ_QUESTION, $quizQuestionInfo);
$this->plugin->addAttemptInQuiz(
LogEvent::STATUS_SUCCESS,
$user->getId(),
$quizQuestionInfo['question'],
$quizQuestionInfo['quiz']
);
$result['resultHtml'] .= '<script>window.location.href = "'.$url.'";</script>';
return $result;
}
if (empty($lpItemInfo) && empty($quizQuestionInfo)) {
$this->plugin->addAuthenticationAttempt(LogEvent::STATUS_SUCCESS, $user->getId());
}
$loggedUser = [
'user_id' => $user->getId(),
'status' => $user->getStatus(),
'uidReset' => true,
];
if (empty($user2fa)) {
ChamiloSession::write(WhispeakAuthPlugin::SESSION_2FA_USER, $user->getId());
}
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_FAILED_LOGINS);
ChamiloSession::write('_user', $loggedUser);
Login::init_user($user->getId(), true);
$result['resultHtml'] .= '<script>window.location.href = "'.api_get_path(WEB_PATH).'";</script>';
return $result;
}
return $result;
}
/**
* {@inheritdoc}
*/
protected function displayPage($isFullPage, array $variables)
{
global $htmlHeadXtra;
$htmlHeadXtra[] = api_get_js('rtc/RecordRTC.js');
$htmlHeadXtra[] = api_get_js_simple(api_get_path(WEB_PLUGIN_PATH).'whispeakauth/assets/js/RecordAudio.js');
$pageTitle = $this->plugin->get_title();
$template = new \Template($pageTitle, $isFullPage, $isFullPage, !$isFullPage);
foreach ($variables as $key => $value) {
$template->assign($key, $value);
}
$pageContent = $template->fetch('whispeakauth/view/authentify_recorder.html.twig');
$template->assign('header', $pageTitle);
$template->assign('content', $pageContent);
$template->display_one_col_template();
}
}

View File

@@ -0,0 +1,67 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\WhispeakAuth\Controller;
use Chamilo\UserBundle\Entity\User;
use FFMpeg\FFMpeg;
use FFMpeg\Format\Audio\Wav;
/**
* Class BaseController.
*
* @package Chamilo\PluginBundle\WhispeakAuth\Controller
*/
abstract class BaseController
{
/**
* @var \WhispeakAuthPlugin
*/
protected $plugin;
/**
* BaseController constructor.
*/
public function __construct()
{
$this->plugin = \WhispeakAuthPlugin::create();
}
/**
* @param bool $isFullPage
*
* @return mixed
*/
abstract protected function displayPage($isFullPage, array $variables);
/**
* @throws \Exception
*
* @return string
*/
protected function uploadAudioFile(User $user)
{
$pluginName = $this->plugin->get_name();
$path = api_upload_file($pluginName, $_FILES['audio'], $user->getId());
if (false === $path) {
throw new \Exception(get_lang('UploadError'));
}
$fullPath = api_get_path(SYS_UPLOAD_PATH).$pluginName.$path['path_to_save'];
$mimeType = mime_content_type($fullPath);
if ('wav' !== substr($mimeType, -3)) {
$ffmpeg = FFMpeg::create();
$audioFile = $ffmpeg->open($fullPath);
$fullPath = dirname($fullPath).'/audio.wav';
$audioFile->save(new Wav(), $fullPath);
}
return $fullPath;
}
}

View File

@@ -0,0 +1,116 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\WhispeakAuth\Controller;
use Chamilo\PluginBundle\WhispeakAuth\Request\ApiRequest;
/**
* Class EnrollmentController.
*
* @package Chamilo\PluginBundle\WhispeakAuth\Controller
*/
class EnrollmentController extends BaseController
{
/**
* @throws \Exception
*/
public function index()
{
if (!$this->plugin->toolIsEnabled()) {
throw new \Exception(get_lang('NotAllowed'));
}
$user = api_get_user_entity(api_get_user_id());
$userIsEnrolled = \WhispeakAuthPlugin::checkUserIsEnrolled($user->getId());
if ($userIsEnrolled) {
throw new \Exception($this->plugin->get_lang('SpeechAuthAlreadyEnrolled'));
}
$request = new ApiRequest();
$response = $request->createEnrollmentSessionToken($user);
\ChamiloSession::write(\WhispeakAuthPlugin::SESSION_SENTENCE_TEXT, $response['token']);
$this->displayPage(
true,
[
'action' => 'enrollment',
'sample_text' => $response['text'],
]
);
}
/**
* @throws \Exception
*/
public function ajax()
{
$result = ['resultHtml' => ''];
if (!$this->plugin->toolIsEnabled() || empty($_FILES['audio'])) {
throw new \Exception(get_lang('NotAllowed'));
}
$user = api_get_user_entity(api_get_user_id());
$audioFilePath = $this->uploadAudioFile($user);
$token = \ChamiloSession::read(\WhispeakAuthPlugin::SESSION_SENTENCE_TEXT);
if (empty($token)) {
throw new \Exception($this->plugin->get_lang('EnrollmentFailed'));
}
$request = new ApiRequest();
try {
$response = $request->createEnrollment($token, $audioFilePath, $user);
} catch (\Exception $exception) {
$enrollTokenRequest = new ApiRequest();
$enrollTokenResponse = $enrollTokenRequest->createEnrollmentSessionToken($user);
\ChamiloSession::write(\WhispeakAuthPlugin::SESSION_SENTENCE_TEXT, $enrollTokenResponse['token']);
return [
'resultHtml' => \Display::return_message($exception->getMessage(), 'error'),
'text' => $enrollTokenResponse['text'],
];
}
\ChamiloSession::erase(\WhispeakAuthPlugin::SESSION_SENTENCE_TEXT);
$this->plugin->saveEnrollment($user, $response['speaker']);
$result['resultHtml'] .= \Display::return_message($this->plugin->get_lang('EnrollmentSuccess'), 'success');
return $result;
}
/**
* {@inheritdoc}
*/
protected function displayPage($isFullPage, array $variables)
{
global $htmlHeadXtra;
$htmlHeadXtra[] = api_get_js('rtc/RecordRTC.js');
$htmlHeadXtra[] = api_get_js_simple(api_get_path(WEB_PLUGIN_PATH).'whispeakauth/assets/js/RecordAudio.js');
$pageTitle = $this->plugin->get_lang('EnrollmentTitle');
$template = new \Template($pageTitle);
foreach ($variables as $key => $value) {
$template->assign($key, $value);
}
$pageContent = $template->fetch('whispeakauth/view/record_audio.html.twig');
$template->assign('header', $pageTitle);
$template->assign('content', $pageContent);
$template->display_one_col_template();
}
}

View File

@@ -0,0 +1,145 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\WhispeakAuth;
use Chamilo\UserBundle\Entity\User;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
/**
* Class LogEvent.
*
* @package Chamilo\PluginBundle\Entity\WhispeakAuth
*
* @ORM\Table(name="whispeak_log_event")
* @ORM\Entity()
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="discr", type="string")
* @ORM\DiscriminatorMap({
* "log_event" = "Chamilo\PluginBundle\Entity\WhispeakAuth\LogEvent",
* "log_event_lp" = "Chamilo\PluginBundle\Entity\WhispeakAuth\LogEventLp",
* "log_event_quiz" = "Chamilo\PluginBundle\Entity\WhispeakAuth\LogEventQuiz"
* })
*/
class LogEvent
{
public const STATUS_FAILED = 0;
public const STATUS_SUCCESS = 1;
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id()
* @ORM\GeneratedValue()
*/
private $id;
/**
* @var DateTime
*
* @ORM\Column(name="datetime", type="datetime")
*/
private $datetime;
/**
* @var int
*
* @ORM\Column(name="action_status", type="smallint")
*/
private $actionStatus;
/**
* @var User
*
* @ORM\ManyToOne(targetEntity="Chamilo\UserBundle\Entity\User")
* @ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=false)
*/
private $user;
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @param int $id
*
* @return LogEvent
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* @return DateTime
*/
public function getDatetime()
{
return $this->datetime;
}
/**
* @param DateTime $datetime
*
* @return LogEvent
*/
public function setDatetime($datetime)
{
$this->datetime = $datetime;
return $this;
}
/**
* @return int
*/
public function getActionStatus()
{
return $this->actionStatus;
}
/**
* @param int $actionStatus
*
* @return LogEvent
*/
public function setActionStatus($actionStatus)
{
$this->actionStatus = $actionStatus;
return $this;
}
/**
* @return User
*/
public function getUser()
{
return $this->user;
}
/**
* @param User $user
*
* @return LogEvent
*/
public function setUser($user)
{
$this->user = $user;
return $this;
}
/**
* @return string
*/
public function getTypeString()
{
return '-';
}
}

View File

@@ -0,0 +1,84 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\WhispeakAuth;
use Chamilo\CourseBundle\Entity\CLp;
use Chamilo\CourseBundle\Entity\CLpItem;
use Doctrine\ORM\Mapping as ORM;
/**
* Class LogEventLp.
*
* @package Chamilo\PluginBundle\Entity\WhispeakAuth
*
* @ORM\Entity()
*/
class LogEventLp extends LogEvent
{
/**
* @var CLpItem
*
* @ORM\ManyToOne(targetEntity="Chamilo\CourseBundle\Entity\CLpItem")
* @ORM\JoinColumn(name="lp_item_id", referencedColumnName="iid")
*/
private $lpItem;
/**
* @var CLp
*
* @ORM\ManyToOne(targetEntity="Chamilo\CourseBundle\Entity\CLp")
* @ORM\JoinColumn(name="lp_id", referencedColumnName="iid")
*/
private $lp;
/**
* @return CLpItem
*/
public function getLpItem()
{
return $this->lpItem;
}
/**
* @param CLpItem $lpItem
*
* @return LogEventLp
*/
public function setLpItem($lpItem)
{
$this->lpItem = $lpItem;
return $this;
}
/**
* @return CLp
*/
public function getLp()
{
return $this->lp;
}
/**
* @param CLp $lp
*
* @return LogEventLp
*/
public function setLp($lp)
{
$this->lp = $lp;
return $this;
}
/**
* {@inheritdoc}
*/
public function getTypeString()
{
$lpName = $this->lp->getName();
$itemTitle = $this->getLpItem()->getTitle();
return "$lpName > $itemTitle";
}
}

View File

@@ -0,0 +1,84 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\WhispeakAuth;
use Chamilo\CourseBundle\Entity\CQuiz;
use Chamilo\CourseBundle\Entity\CQuizQuestion;
use Doctrine\ORM\Mapping as ORM;
/**
* Class LogEventQuiz.
*
* @package Chamilo\PluginBundle\Entity\WhispeakAuth
*
* @ORM\Entity()
*/
class LogEventQuiz extends LogEvent
{
/**
* @var CQuizQuestion
*
* @ORM\ManyToOne(targetEntity="Chamilo\CourseBundle\Entity\CQuizQuestion")
* @ORM\JoinColumn(name="question_id", referencedColumnName="iid")
*/
private $question;
/**
* @var CQuiz
*
* @ORM\ManyToOne(targetEntity="Chamilo\CourseBundle\Entity\CQuiz")
* @ORM\JoinColumn(name="quiz_id", referencedColumnName="iid")
*/
private $quiz;
/**
* @return CQuizQuestion
*/
public function getQuestion()
{
return $this->question;
}
/**
* @param CQuizQuestion $question
*
* @return LogEventQuiz
*/
public function setQuestion($question)
{
$this->question = $question;
return $this;
}
/**
* @return CQuiz
*/
public function getQuiz()
{
return $this->quiz;
}
/**
* @param CQuiz $quiz
*
* @return LogEventQuiz
*/
public function setQuiz($quiz)
{
$this->quiz = $quiz;
return $this;
}
/**
* {@inheritdoc}
*/
public function getTypeString()
{
$quiz = strip_tags($this->getQuiz()->getTitle());
$question = strip_tags($this->getQuestion()->getQuestion());
return "$quiz > $question";
}
}

View File

@@ -0,0 +1,23 @@
Speech authentication with Whispeak
===================================
**Notice:**
This plugin requires the user to grant permission to use the microphone connected on the web browser. Currently,
browsers are limiting this permission to be used only in a secure environment with HTTPS.
**If your portal does not work with HTTPS, then Whispeak authentication may not work.**
Installation:
-------------
*Prior to installing/uninstalling this plugin, you will need to make sure the src/Chamilo/PluginBundle/Entity folder is
temporarily writeable by the web server. This might imply a manual change on your server (outside of the Chamilo
interface).*
1. Install plugin in Chamilo.
2. Set the plugin configuration enabling the plugin and (optionally) set the max attempts.
3. Set the `login_bottom` region to the plugin.
4. Add `$_configuration['whispeak_auth_enabled'] = true;` to `configuration.php` file.
5. Optionally, you can add the `menu_administrator` region to se the user logged activities from Whispeak.
To have more information about whispeak or create an account to be able to use it on Chamilo you can go here <a href="https://whispeak.io/elearning/?source=chamilo">Whispeak</a>

View File

@@ -0,0 +1,230 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\WhispeakAuth\Request;
use Chamilo\UserBundle\Entity\User;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
/**
* Class ApiRequest.
*
* @package Chamilo\PluginBundle\WhispeakAuth\Request
*/
class ApiRequest
{
/**
* @var \WhispeakAuthPlugin
*/
protected $plugin;
/**
* @var string
*/
protected $apiKey;
/**
* BaseController constructor.
*/
public function __construct()
{
$this->plugin = \WhispeakAuthPlugin::create();
$this->apiKey = $this->plugin->get(\WhispeakAuthPlugin::SETTING_TOKEN);
}
/**
* Create a session token to perform an enrollment.
*
* @throws \Exception
*
* @return array
*/
public function createEnrollmentSessionToken(User $user)
{
$apiKey = $this->plugin->get(\WhispeakAuthPlugin::SETTING_TOKEN);
$langIso = api_get_language_isocode($user->getLanguage());
return $this->sendRequest(
'get',
'enroll',
$apiKey,
$langIso
);
}
/**
* @param string $token
* @param string $audioFilePath
*
* @throws \Exception
*
* @return array
*/
public function createEnrollment($token, $audioFilePath, User $user)
{
$langIso = api_get_language_isocode($user->getLanguage());
return $this->sendRequest(
'post',
'enroll',
$token,
$langIso,
[
[
'name' => 'file',
'contents' => fopen($audioFilePath, 'r'),
'filename' => basename($audioFilePath),
],
]
);
}
/**
* @throws \Exception
*
* @return array
*/
public function createAuthenticationSessionToken(User $user = null)
{
$apiKey = $this->plugin->get(\WhispeakAuthPlugin::SETTING_TOKEN);
$langIso = api_get_language_isocode($user ? $user->getLanguage() : null);
return $this->sendRequest(
'get',
'auth',
$apiKey,
$langIso
);
}
/**
* @throws \Exception
*
* @return array
*/
public function deleteEnrollment(User $user)
{
$apiKey = $this->plugin->get(\WhispeakAuthPlugin::SETTING_TOKEN);
$langIso = api_get_language_isocode($user->getLanguage());
$userAuthKey = \WhispeakAuthPlugin::getAuthUidValue($user->getId());
if (empty($userAuthKey) || empty($userAuthKey->getValue())) {
throw new \Exception(get_plugin_lang('NoEnrollment', 'WhispeakAuthPlugin'));
}
$queryData = ['speaker' => $userAuthKey->getValue()];
return $this->sendRequest(
'delete',
'enroll',
$apiKey,
$langIso,
[],
$queryData
);
}
/**
* @param string $token
* @param string $audioFilePath
*
* @throws \Exception
*
* @return bool
*/
public function performAuthentication($token, User $user, $audioFilePath)
{
$wsid = \WhispeakAuthPlugin::getAuthUidValue($user->getId());
if (empty($wsid)) {
throw new \Exception($this->plugin->get_lang('SpeechAuthNotEnrolled'));
}
$langIso = api_get_language_isocode($user ? $user->getLanguage() : null);
$this->sendRequest(
'post',
'auth',
$token,
$langIso,
[
[
'name' => 'speaker',
'contents' => $wsid->getValue(),
],
[
'name' => 'file',
'contents' => fopen($audioFilePath, 'r'),
'filename' => basename($audioFilePath),
],
]
);
}
/**
* @param string $method
* @param string $uri
* @param string $authBearer
* @param string $lang
* @param array $queryParams
*
* @throws \GuzzleHttp\Exception\GuzzleException
*
* @return array
*/
private function sendRequest($method, $uri, $authBearer, $lang, array $multipart = [], $queryParams = [])
{
$httpClient = new Client(['base_uri' => $this->plugin->getApiUrl()]);
$options = [];
$options['headers'] = [
'Authorization' => "Bearer $authBearer",
'Accept-Language' => $lang,
];
if ($queryParams) {
$options['query'] = $queryParams;
} else {
$options['multipart'] = $multipart;
}
try {
$responseBody = $httpClient
->request(
$method,
$uri,
$options
)
->getBody()
->getContents();
return json_decode($responseBody, true);
} catch (RequestException $requestException) {
if (!$requestException->hasResponse()) {
throw new \Exception($requestException->getMessage());
}
$responseBody = $requestException->getResponse()->getBody()->getContents();
$json = json_decode($responseBody, true);
$message = '';
if (isset($json['asserts'])) {
foreach ($json['asserts'] as $assert) {
if ('invalid_' === substr($assert['value'], 0, 8)) {
$message .= $assert['message'].PHP_EOL;
}
}
} elseif (empty($json['message'])) {
$message = $requestException->getMessage();
} else {
$message = is_array($json['message']) ? implode(PHP_EOL, $json['message']) : $json['message'];
}
throw new \Exception($message);
} catch (Exception $exception) {
throw new \Exception($exception->getMessage());
}
}
}

View File

@@ -0,0 +1,804 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\ExtraField;
use Chamilo\CoreBundle\Entity\ExtraFieldValues;
use Chamilo\PluginBundle\Entity\WhispeakAuth\LogEvent;
use Chamilo\PluginBundle\Entity\WhispeakAuth\LogEventLp;
use Chamilo\PluginBundle\Entity\WhispeakAuth\LogEventQuiz;
use Chamilo\UserBundle\Entity\User;
use Doctrine\ORM\Tools\SchemaTool;
/**
* Class WhispeakAuthPlugin.
*/
class WhispeakAuthPlugin extends Plugin implements HookPluginInterface
{
public const SETTING_ENABLE = 'enable';
public const SETTING_MAX_ATTEMPTS = 'max_attempts';
public const SETTING_2FA = '2fa';
public const SETTING_API_URL = 'api_url';
public const SETTING_TOKEN = 'token';
public const EXTRAFIELD_AUTH_UID = 'whispeak_auth_uid';
public const EXTRAFIELD_LP_ITEM = 'whispeak_lp_item';
public const EXTRAFIELD_QUIZ_QUESTION = 'whispeak_quiz_question';
public const SESSION_FAILED_LOGINS = 'whispeak_failed_logins';
public const SESSION_2FA_USER = 'whispeak_user_id';
public const SESSION_LP_ITEM = 'whispeak_lp_item';
public const SESSION_QUIZ_QUESTION = 'whispeak_quiz_question';
public const SESSION_AUTH_PASSWORD = 'whispeak_auth_password';
public const SESSION_SENTENCE_TEXT = 'whispeak_sentence_text';
/**
* 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_MAX_ATTEMPTS => 'text',
self::SETTING_2FA => 'boolean',
]
);
}
/**
* Get the admin URL for the plugin if Plugin::isAdminPlugin is true.
*
* @return string
*/
public function getAdminUrl()
{
$webPath = api_get_path(WEB_PLUGIN_PATH).$this->get_name();
return "$webPath/admin.php";
}
/**
* @return WhispeakAuthPlugin
*/
public static function create()
{
static $result = null;
return $result ? $result : $result = new self();
}
/**
* @throws \Doctrine\ORM\Tools\ToolsException
*/
public function install()
{
$this->installExtraFields();
$this->installEntities();
$this->installHook();
}
public function uninstall()
{
$this->uninstallHook();
$this->uninstallExtraFields();
$this->uninstallEntities();
}
/**
* @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;
}
/**
* @return ExtraField
*/
public static function getLpItemExtraField()
{
$efRepo = Database::getManager()->getRepository('ChamiloCoreBundle:ExtraField');
/** @var ExtraField $extraField */
$extraField = $efRepo->findOneBy(
[
'variable' => self::EXTRAFIELD_LP_ITEM,
'extraFieldType' => ExtraField::LP_ITEM_FIELD_TYPE,
]
);
return $extraField;
}
/**
* @return ExtraField
*/
public static function getQuizQuestionExtraField()
{
$efRepo = Database::getManager()->getRepository('ChamiloCoreBundle:ExtraField');
/** @var ExtraField $extraField */
$extraField = $efRepo->findOneBy(
[
'variable' => self::EXTRAFIELD_QUIZ_QUESTION,
'extraFieldType' => ExtraField::QUESTION_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;
}
/**
* Get the whispeak_lp_item value for a LP item ID.
*
* @param int $lpItemId
*
* @return array|false
*/
public static function getLpItemValue($lpItemId)
{
$efv = new ExtraFieldValue('lp_item');
$value = $efv->get_values_by_handler_and_field_variable($lpItemId, self::EXTRAFIELD_LP_ITEM);
return $value;
}
/**
* @param int $lpItemId
*
* @return bool
*/
public static function isLpItemMarked($lpItemId)
{
if (!self::create()->isEnabled()) {
return false;
}
$value = self::getLpItemValue($lpItemId);
return !empty($value) && !empty($value['value']);
}
/**
* Get the whispeak_quiz_question value for a quiz question ID.
*
* @param int $questionId
*
* @return array|false
*/
public static function getQuizQuestionValue($questionId)
{
$efv = new ExtraFieldValue('question');
$value = $efv->get_values_by_handler_and_field_variable($questionId, self::EXTRAFIELD_QUIZ_QUESTION);
return $value;
}
/**
* @param int $questionId
*
* @return bool
*/
public static function isQuizQuestionMarked($questionId)
{
if (!self::create()->isEnabled()) {
return false;
}
$value = self::getQuizQuestionValue($questionId);
return !empty($value) && !empty($value['value']);
}
/**
* @param int $questionId
*
* @return bool
*/
public static function questionRequireAuthentify($questionId)
{
$isMarked = self::isQuizQuestionMarked($questionId);
if (!$isMarked) {
return false;
}
$questionInfo = ChamiloSession::read(self::SESSION_QUIZ_QUESTION, []);
if (empty($questionInfo)) {
return true;
}
if ((int) $questionId !== $questionInfo['question']) {
return true;
}
if (false === $questionInfo['passed']) {
return true;
}
return false;
}
/**
* @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 string $uid
*
* @throws \Doctrine\ORM\OptimisticLockException
*/
public function saveEnrollment(User $user, $uid)
{
$em = Database::getManager();
$extraFieldValue = self::getAuthUidValue($user->getId());
if (empty($extraFieldValue)) {
$extraField = self::getAuthUidExtraField();
$now = new DateTime('now', new DateTimeZone('UTC'));
$extraFieldValue = new ExtraFieldValues();
$extraFieldValue
->setField($extraField)
->setItemId($user->getId())
->setUpdatedAt($now);
}
$extraFieldValue->setValue($uid);
$em->persist($extraFieldValue);
$em->flush();
}
/**
* @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);
}
/**
* Get the max_attemtps option.
*
* @return int
*/
public function getMaxAttempts()
{
return (int) $this->get(self::SETTING_MAX_ATTEMPTS);
}
/**
* Install hook when saving the plugin configuration.
*
* @return WhispeakAuthPlugin
*/
public function performActionsAfterConfigure()
{
$observer = WhispeakConditionalLoginHook::create();
if ('true' === $this->get(self::SETTING_2FA)) {
HookConditionalLogin::create()->attach($observer);
} else {
HookConditionalLogin::create()->detach($observer);
}
return $this;
}
/**
* This method will call the Hook management insertHook to add Hook observer from this plugin.
*/
public function installHook()
{
$observer = WhispeakMyStudentsLpTrackingHook::create();
HookMyStudentsLpTracking::create()->attach($observer);
$observer = WhispeakMyStudentsQuizTrackingHook::create();
HookMyStudentsQuizTracking::create()->attach($observer);
}
/**
* This method will call the Hook management deleteHook to disable Hook observer from this plugin.
*/
public function uninstallHook()
{
$observer = WhispeakConditionalLoginHook::create();
HookConditionalLogin::create()->detach($observer);
$observer = WhispeakMyStudentsLpTrackingHook::create();
HookMyStudentsLpTracking::create()->detach($observer);
}
/**
* @param int $userId
*
* @throws \Doctrine\ORM\OptimisticLockException
*
* @return bool
*/
public static function deleteEnrollment($userId)
{
$extraFieldValue = self::getAuthUidValue($userId);
if (empty($extraFieldValue)) {
return false;
}
$em = Database::getManager();
$em->remove($extraFieldValue);
$em->flush();
return true;
}
/**
* Check if the WhispeakAuth plugin is installed and enabled.
*
* @param bool $checkEnabled Check if, additionnally to being installed, the plugin is enabled
*/
public function isEnabled(bool $checkEnabled = false): bool
{
return parent::isEnabled() && 'true' === api_get_plugin_setting('whispeakauth', self::SETTING_ENABLE);
}
/**
* @param int $lpItemId
*
* @return bool
*/
public static function isAllowedToSaveLpItem($lpItemId)
{
if (!self::isLpItemMarked($lpItemId)) {
return true;
}
$markedItem = ChamiloSession::read(self::SESSION_LP_ITEM, []);
if (empty($markedItem)) {
return true;
}
if ((int) $lpItemId !== (int) $markedItem['lp_item']) {
return true;
}
return false;
}
/**
* Display a error message.
*
* @param string|null $error Optional. The message text
*/
public static function displayNotAllowedMessage($error = null)
{
$error = empty($error) ? get_lang('NotAllowed') : $error;
echo Display::return_message($error, 'error', false);
exit;
}
/**
* @param int $questionId
*
* @throws Exception
*
* @return string
*/
public static function quizQuestionAuthentify($questionId, Exercise $exercise)
{
ChamiloSession::write(
self::SESSION_QUIZ_QUESTION,
[
'quiz' => (int) $exercise->iid,
'question' => (int) $questionId,
'url_params' => $_SERVER['QUERY_STRING'],
'passed' => false,
]
);
$template = new Template('', false, false, false, true, false, false);
$template->assign('question', $questionId);
$template->assign('exercise', $exercise->iid);
$content = $template->fetch('whispeakauth/view/quiz_question.html.twig');
echo $content;
}
/**
* @param int $status
* @param int $userId
* @param int $lpItemId
* @param int $lpId
*
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\TransactionRequiredException
*
* @return LogEventLp|null
*/
public function addAttemptInLearningPath($status, $userId, $lpItemId, $lpId)
{
$em = Database::getManager();
$user = api_get_user_entity($userId);
$lpItem = $em->find('ChamiloCourseBundle:CLpItem', $lpItemId);
$lp = $em->find('ChamiloCourseBundle:CLp', $lpId);
if (empty($lp) || empty($lpItem)) {
return null;
}
$logEvent = new LogEventLp();
$logEvent
->setLpItem($lpItem)
->setLp($lp)
->setUser($user)
->setDatetime(
api_get_utc_datetime(null, false, true)
)
->setActionStatus($status);
$em->persist($logEvent);
$em->flush();
return $logEvent;
}
/**
* @param int $status
* @param int $userId
* @param int $questionId
* @param int $quizId
*
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\TransactionRequiredException
*
* @return LogEventQuiz|null
*/
public function addAttemptInQuiz($status, $userId, $questionId, $quizId)
{
$em = Database::getManager();
$user = api_get_user_entity($userId);
$question = $em->find('ChamiloCourseBundle:CQuizQuestion', $questionId);
$quiz = $em->find('ChamiloCourseBundle:CQuiz', $quizId);
if (empty($quiz) || empty($question)) {
return null;
}
$logEvent = new LogEventQuiz();
$logEvent
->setQuestion($question)
->setQuiz($quiz)
->setUser($user)
->setDatetime(
api_get_utc_datetime(null, false, true)
)
->setActionStatus($status);
$em->persist($logEvent);
$em->flush();
return $logEvent;
}
/**
* @param int $status
* @param int $userId
*
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\TransactionRequiredException
*
* @return LogEvent|null
*/
public function addAuthenticationAttempt($status, $userId)
{
$em = Database::getManager();
$user = api_get_user_entity($userId);
$logEvent = new LogEvent();
$logEvent
->setUser($user)
->setDatetime(
api_get_utc_datetime(null, false, true)
)
->setActionStatus($status);
$em->persist($logEvent);
$em->flush();
return $logEvent;
}
/**
* @param int $lpId
* @param int $userId
*
* @throws \Doctrine\ORM\Query\QueryException
*
* @return string
*/
public static function countAllAttemptsInLearningPath($lpId, $userId)
{
$query = Database::getManager()
->createQuery(
'SELECT COUNT(log) AS c FROM ChamiloPluginBundle:WhispeakAuth\LogEventLp log
WHERE log.lp = :lp AND log.user = :user'
)
->setParameters(['lp' => $lpId, 'user' => $userId]);
$totalCount = (int) $query->getSingleScalarResult();
return $totalCount;
}
/**
* @param int $lpId
* @param int $userId
*
* @throws \Doctrine\ORM\Query\QueryException
*
* @return string
*/
public static function countSuccessAttemptsInLearningPath($lpId, $userId)
{
$query = Database::getManager()
->createQuery(
'SELECT COUNT(log) AS c FROM ChamiloPluginBundle:WhispeakAuth\LogEventLp log
WHERE log.lp = :lp AND log.user = :user AND log.actionStatus = :status'
)
->setParameters(['lp' => $lpId, 'user' => $userId, 'status' => LogEvent::STATUS_SUCCESS]);
$totalCount = (int) $query->getSingleScalarResult();
return $totalCount;
}
/**
* @param int $quizId
* @param int $userId
*
* @throws \Doctrine\ORM\Query\QueryException
*
* @return string
*/
public static function countAllAttemptsInQuiz($quizId, $userId)
{
$query = Database::getManager()
->createQuery(
'SELECT COUNT(log) AS c FROM ChamiloPluginBundle:WhispeakAuth\LogEventQuiz log
WHERE log.quiz = :quiz AND log.user = :user'
)
->setParameters(['quiz' => $quizId, 'user' => $userId]);
$totalCount = (int) $query->getSingleScalarResult();
return $totalCount;
}
/**
* @param int $quizId
* @param int $userId
*
* @throws \Doctrine\ORM\Query\QueryException
*
* @return string
*/
public static function countSuccessAttemptsInQuiz($quizId, $userId)
{
$query = Database::getManager()
->createQuery(
'SELECT COUNT(log) AS c FROM ChamiloPluginBundle:WhispeakAuth\LogEventQuiz log
WHERE log.quiz = :quiz AND log.user = :user AND log.actionStatus = :status'
)
->setParameters(['quiz' => $quizId, 'user' => $userId, 'status' => LogEvent::STATUS_SUCCESS]);
$totalCount = (int) $query->getSingleScalarResult();
return $totalCount;
}
/**
* @return string
*/
public function getApiUrl()
{
$url = $this->get(self::SETTING_API_URL);
return trim($url, " \t\n\r \v/").'/';
}
/**
* Install extra fields for user, learning path and quiz question.
*/
private function installExtraFields()
{
UserManager::create_extra_field(
self::EXTRAFIELD_AUTH_UID,
\ExtraField::FIELD_TYPE_TEXT,
$this->get_lang('Whispeak uid'),
''
);
LpItem::createExtraField(
self::EXTRAFIELD_LP_ITEM,
\ExtraField::FIELD_TYPE_CHECKBOX,
$this->get_lang('MarkForSpeechAuthentication'),
'0',
true,
true
);
$extraField = new \ExtraField('question');
$params = [
'variable' => self::EXTRAFIELD_QUIZ_QUESTION,
'field_type' => \ExtraField::FIELD_TYPE_CHECKBOX,
'display_text' => $this->get_lang('MarkForSpeechAuthentication'),
'default_value' => '0',
'changeable' => true,
'visible_to_self' => true,
'visible_to_others' => false,
];
$extraField->save($params);
}
/**
* Install the Doctrine's entities.
*
* @throws \Doctrine\ORM\Tools\ToolsException
*/
private function installEntities()
{
$em = Database::getManager();
if ($em->getConnection()->getSchemaManager()->tablesExist(['whispeak_log_event'])) {
return;
}
$schemaTool = new SchemaTool($em);
$schemaTool->createSchema(
[
$em->getClassMetadata(LogEvent::class),
$em->getClassMetadata(LogEventLp::class),
$em->getClassMetadata(LogEventQuiz::class),
]
);
}
/**
* Uninstall extra fields for user, learning path and quiz question.
*/
private function uninstallExtraFields()
{
$em = Database::getManager();
$authIdExtrafield = self::getAuthUidExtraField();
if (!empty($authIdExtrafield)) {
$em
->createQuery('DELETE FROM ChamiloCoreBundle:ExtraFieldValues efv WHERE efv.field = :field')
->execute(['field' => $authIdExtrafield]);
$em->remove($authIdExtrafield);
$em->flush();
}
$lpItemExtrafield = self::getLpItemExtraField();
if (!empty($lpItemExtrafield)) {
$em
->createQuery('DELETE FROM ChamiloCoreBundle:ExtraFieldValues efv WHERE efv.field = :field')
->execute(['field' => $lpItemExtrafield]);
$em->remove($lpItemExtrafield);
$em->flush();
}
$quizQuestionExtrafield = self::getQuizQuestionExtraField();
if (!empty($quizQuestionExtrafield)) {
$em
->createQuery('DELETE FROM ChamiloCoreBundle:ExtraFieldValues efv WHERE efv.field = :field')
->execute(['field' => $quizQuestionExtrafield]);
$em->remove($quizQuestionExtrafield);
$em->flush();
}
}
/**
* Uninstall the Doctrine's entities.
*/
private function uninstallEntities()
{
$em = Database::getManager();
if (!$em->getConnection()->getSchemaManager()->tablesExist(['whispeak_log_event'])) {
return;
}
$schemaTool = new SchemaTool($em);
$schemaTool->dropSchema(
[
$em->getClassMetadata(LogEvent::class),
$em->getClassMetadata(LogEventLp::class),
$em->getClassMetadata(LogEventQuiz::class),
]
);
}
}

View File

@@ -0,0 +1,59 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class WhispeakConditionalLoginHook.
*
* Implements a Two-Factor Authentication with Whispeak.
*/
class WhispeakConditionalLoginHook extends HookObserver implements HookConditionalLoginObserverInterface
{
/**
* WhispeakConditionalLoginHook constructor.
*/
protected function __construct()
{
parent::__construct(
'plugin/whispeakauth/WhispeakAuthPlugin.php',
'whispeakauth'
);
}
/**
* Return an associative array (callable, url) needed for Conditional Login.
* <code>
* [
* 'conditional_function' => function (array $userInfo) {},
* 'url' => '',
* ]
* </code>
* conditional_function returns false to redirect to the url and returns true to continue with the classical login.
*
* @return array
*/
public function hookConditionalLogin(HookConditionalLoginEventInterface $hook)
{
return [
'conditional_function' => function (array $userInfo) {
$isEnrolled = WhispeakAuthPlugin::checkUserIsEnrolled($userInfo['user_id']);
if (!$isEnrolled) {
return true;
}
$user2fa = (int) ChamiloSession::read(WhispeakAuthPlugin::SESSION_2FA_USER, 0);
if ($user2fa === (int) $userInfo['user_id']) {
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_2FA_USER);
return true;
}
ChamiloSession::write(WhispeakAuthPlugin::SESSION_2FA_USER, $userInfo['user_id']);
return false;
},
'url' => api_get_path(WEB_PLUGIN_PATH).$this->getPluginName().'/authentify.php',
];
}
}

View File

@@ -0,0 +1,67 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class WhispeakMyStudentsLpTrackingHook.
*/
class WhispeakMyStudentsLpTrackingHook extends HookObserver implements HookMyStudentsLpTrackingObserverInterface
{
/**
* WhispeakMyStudentsLpTrackingHook constructor.
*/
protected function __construct()
{
parent::__construct(
'plugin/whispeakauth/WhispeakAuthPlugin.php',
'whispeakauth'
);
}
/**
* @return array
*/
public function trackingHeader(HookMyStudentsLpTrackingEventInterface $hook)
{
if (false === WhispeakAuthPlugin::create()->isEnabled()) {
return [];
}
return [
'value' => WhispeakAuthPlugin::create()->get_lang('plugin_title'),
'attrs' => ['class' => 'text-center'],
];
}
/**
* @throws \Doctrine\ORM\Query\QueryException
*
* @return array
*/
public function trackingContent(HookMyStudentsLpTrackingEventInterface $hook)
{
if (false === WhispeakAuthPlugin::create()->isEnabled()) {
return [];
}
$data = $hook->getEventData();
$totalCount = WhispeakAuthPlugin::countAllAttemptsInLearningPath($data['lp_id'], $data['student_id']);
if (0 === $totalCount) {
return [
'value' => '-',
'attrs' => ['class' => 'text-center'],
];
}
$successCount = WhispeakAuthPlugin::countSuccessAttemptsInLearningPath($data['lp_id'], $data['student_id']);
$attrs = ['class' => 'text-center '];
$attrs['class'] .= $successCount <= $totalCount / 2 ? 'text-danger' : 'text-success';
return [
'value' => Display::tag('strong', "$successCount / $totalCount"),
'attrs' => $attrs,
];
}
}

View File

@@ -0,0 +1,84 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class WhispeakMyStudentsQuizTrackingHook.
*/
class WhispeakMyStudentsQuizTrackingHook extends HookObserver implements HookMyStudentsQuizTrackingObserverInterface
{
/**
* WhispeakMyStudentsQuizTrackingHook constructor.
*/
protected function __construct()
{
parent::__construct(
'plugin/whispeakauth/WhispeakAuthPlugin.php',
'whispeakauth'
);
}
/**
* Return an associative array this value and attributes.
* <code>
* [
* 'value' => 'Users online',
* 'attrs' => ['class' => 'text-center'],
* ]
* </code>.
*
* @return array
*/
public function trackingHeader(HookMyStudentsQuizTrackingEventInterface $hook)
{
if (false === WhispeakAuthPlugin::create()->isEnabled()) {
return [];
}
return [
'value' => WhispeakAuthPlugin::create()->get_lang('plugin_title'),
'attrs' => [
'class' => 'text-center',
],
];
}
/**
* Return an associative array this value and attributes.
* <code>
* [
* 'value' => '5 connected users ',
* 'attrs' => ['class' => 'text-center text-success'],
* ]
* </code>.
*
* @throws \Doctrine\ORM\Query\QueryException
*
* @return array
*/
public function trackingContent(HookMyStudentsQuizTrackingEventInterface $hook)
{
if (false === WhispeakAuthPlugin::create()->isEnabled()) {
return [];
}
$data = $hook->getEventData();
$totalCount = WhispeakAuthPlugin::countAllAttemptsInQuiz($data['quiz_id'], $data['student_id']);
if (0 === $totalCount) {
return [
'value' => '-',
'attrs' => ['class' => 'text-center'],
];
}
$successCount = WhispeakAuthPlugin::countSuccessAttemptsInQuiz($data['quiz_id'], $data['student_id']);
$attrs = ['class' => 'text-center '];
$attrs['class'] .= $successCount <= $totalCount / 2 ? 'text-danger' : 'text-success';
return [
'value' => Display::tag('strong', "$successCount / $totalCount"),
'attrs' => $attrs,
];
}
}

View File

@@ -0,0 +1,149 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\WhispeakAuth\LogEvent;
use Chamilo\PluginBundle\Entity\WhispeakAuth\LogEventLp;
use Chamilo\PluginBundle\Entity\WhispeakAuth\LogEventQuiz;
$cidReset = true;
require_once __DIR__.'/../../main/inc/global.inc.php';
$plugin = WhispeakAuthPlugin::create();
api_protect_admin_script(true);
$plugin->protectTool();
$form = new FormValidator('frm_filter', 'GET');
$form->addHeader($plugin->get_lang('ActionRegistryPerUser'));
$slctUsers = $form->addSelectAjax(
'users',
get_lang('Users'),
[],
[
'url' => api_get_path(WEB_AJAX_PATH).'user_manager.ajax.php?a=get_user_like',
'id' => 'user_id',
'multiple' => true,
]
);
$form->addDatePicker('date', get_lang('Date'));
$form->addButtonSearch(get_lang('Search'));
$form->addRule('users', get_lang('ThisFieldIsRequired'), 'required');
$form->addRule('date', get_lang('ThisFieldIsRequired'), 'required');
$results = [];
if ($form->validate()) {
$formValues = $form->exportValues();
$userIds = $formValues['users'] ?: [];
/** @var \DateTime $date */
$starDate = api_get_utc_datetime($formValues['date'], true, true);
$endDate = clone $starDate;
$endDate->modify('next day');
$em = Database::getManager();
$repo = $em->getRepository('ChamiloPluginBundle:WhispeakAuth\LogEvent');
foreach ($userIds as $userId) {
$qb = $em->createQueryBuilder();
$results[$userId] = $qb
->select('event')
->from('ChamiloPluginBundle:WhispeakAuth\LogEvent', 'event')
->where(
$qb->expr()->gte('event.datetime', ':start_date')
)
->andWhere(
$qb->expr()->lt('event.datetime', ':end_date')
)
->andWhere(
$qb->expr()->eq('event.user', ':user')
)
->setParameters(
[
'start_date' => $starDate->format('Y-m-d H:i:s'),
'end_date' => $endDate->format('Y-m-d H:i:s'),
'user' => $userId,
]
)
->getQuery()
->getResult();
}
}
$pageContent = '';
/**
* @var int $userId
* @var array|LogEvent[] $logEvents
*/
foreach ($results as $userId => $logEvents) {
if (empty($logEvents)) {
continue;
}
$user = $logEvents[0]->getUser();
$slctUsers->addOption($user->getCompleteNameWithUsername(), $user->getId());
$tableHeaders = [get_lang('DateTime'), get_lang('Type'), get_lang('Item'), get_lang('Result')];
$tableData = [];
foreach ($logEvents as $i => $logEvent) {
$type = '';
switch (get_class($logEvent)) {
case LogEventQuiz::class:
$type = get_lang('Question');
break;
case LogEventLp::class:
$type = get_lang('LearningPath');
break;
}
$tableData[] = [
api_convert_and_format_date($logEvent->getDatetime(), DATE_TIME_FORMAT_SHORT),
$type,
$logEvent->getTypeString(),
$logEvent->getActionStatus() === LogEvent::STATUS_SUCCESS
? Display::span(get_lang('Success'), ['class' => 'text-success'])
: Display::span(get_lang('Failed'), ['class' => 'text-danger']),
];
}
$table = new HTML_Table(['class' => 'data_table table table-bordered table-hover table-striped table-condensed']);
$table->setHeaders($tableHeaders);
$table->setData($tableData);
$table->updateColAttributes(0, ['class' => 'text-center']);
$table->updateColAttributes(3, ['class' => 'text-center']);
$pageContent .= Display::page_subheader($user->getCompleteNameWithUsername(), null, 'h4');
$pageContent .= $table->toHtml();
}
$interbreadcrumb[] = [
'name' => get_lang('Administration'),
'url' => api_get_path(WEB_CODE_PATH).'admin/index.php',
];
$actionsLeft = '';
if (!empty($results)) {
$actionsLeft = Display::url(
Display::return_icon('back.png', $plugin->get_lang('Back'), [], ICON_SIZE_MEDIUM),
api_get_self()
);
}
$actionsRight = Display::url(
Display::return_icon('delete_terms.png', $plugin->get_lang('Revocation'), [], ICON_SIZE_MEDIUM),
'revocation.php'
);
$template = new Template($plugin->get_title());
$template->assign('actions', Display::toolbarAction('whispeak_admin', [$actionsLeft, $actionsRight]));
$template->assign(
'content',
$form->returnForm().PHP_EOL.$pageContent
);
$template->display_one_col_template();

View File

@@ -0,0 +1,140 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\WhispeakAuth\LogEvent;
require_once __DIR__.'/../../../main/inc/global.inc.php';
api_block_anonymous_users(false);
$plugin = WhispeakAuthPlugin::create();
$plugin->protectTool(false);
$tokenIsValid = Security::check_token();
if (!$tokenIsValid) {
WhispeakAuthPlugin::displayNotAllowedMessage();
}
$maxAttempts = $plugin->getMaxAttempts();
$failedLogins = ChamiloSession::read(WhispeakAuthPlugin::SESSION_FAILED_LOGINS, 0);
if ($maxAttempts && $failedLogins >= $maxAttempts) {
echo Display::return_message($plugin->get_lang('MaxAttemptsReached'), 'warning');
exit;
}
$user = api_get_user_entity(api_get_user_id());
$password = isset($_POST['password']) ? $_POST['password'] : null;
if (empty($password) || empty($user)) {
WhispeakAuthPlugin::displayNotAllowedMessage();
}
if (!in_array($user->getAuthSource(), [PLATFORM_AUTH_SOURCE, CAS_AUTH_SOURCE])) {
WhispeakAuthPlugin::displayNotAllowedMessage();
}
/** @var array $lpItemInfo */
$lpItemInfo = ChamiloSession::read(WhispeakAuthPlugin::SESSION_LP_ITEM, []);
/** @var array $quizQuestionInfo */
$quizQuestionInfo = ChamiloSession::read(WhispeakAuthPlugin::SESSION_QUIZ_QUESTION, []);
$isValidPassword = UserManager::checkPassword($user->getPassword(), $password, $user->getSalt(), $user->getId());
$isActive = $user->isActive();
$isExpired = empty($user->getExpirationDate()) || $user->getExpirationDate() > api_get_utc_datetime(null, false, true);
$userPass = true;
if (!$isValidPassword || !$isActive || !$isExpired) {
if (!empty($lpItemInfo)) {
$plugin->addAttemptInLearningPath(
LogEvent::STATUS_FAILED,
$user->getId(),
$lpItemInfo['lp_item'],
$lpItemInfo['lp']
);
} elseif (!empty($quizQuestionInfo)) {
$plugin->addAttemptInQuiz(
LogEvent::STATUS_FAILED,
$user->getId(),
$quizQuestionInfo['question'],
$quizQuestionInfo['quiz']
);
}
$userPass = false;
$message = $plugin->get_lang('AuthentifyFailed');
if (!$isActive) {
$message .= PHP_EOL.get_lang('Account inactive');
}
if (!$isExpired) {
$message .= PHP_EOL.get_lang('AccountExpired');
}
ChamiloSession::write(WhispeakAuthPlugin::SESSION_FAILED_LOGINS, ++$failedLogins);
if ($maxAttempts && $failedLogins >= $maxAttempts) {
$message .= PHP_EOL.'<span data-reach-attempts="true">'.$plugin->get_lang('MaxAttemptsReached').'</span>';
} else {
$message .= PHP_EOL.$plugin->get_lang('TryAgain');
}
echo Display::return_message($message, 'error', false);
if (!$maxAttempts ||
($maxAttempts && $failedLogins >= $maxAttempts)
) {
$userPass = true;
}
} elseif ($isValidPassword) {
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_FAILED_LOGINS);
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_2FA_USER);
if (!empty($lpItemInfo)) {
$plugin->addAttemptInLearningPath(
LogEvent::STATUS_SUCCESS,
$user->getId(),
$lpItemInfo['lp_item'],
$lpItemInfo['lp']
);
} elseif (!empty($quizQuestionInfo)) {
$plugin->addAttemptInQuiz(
LogEvent::STATUS_SUCCESS,
$user->getId(),
$quizQuestionInfo['question'],
$quizQuestionInfo['quiz']
);
}
echo Display::return_message($plugin->get_lang('AuthentifySuccess'), 'success');
}
if ($userPass) {
$url = '';
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_FAILED_LOGINS);
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_2FA_USER);
if ($lpItemInfo) {
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_LP_ITEM);
$url = $lpItemInfo['src'];
} elseif ($quizQuestionInfo) {
$quizQuestionInfo['passed'] = true;
$url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit.php?'.$quizQuestionInfo['url_params'];
ChamiloSession::write(WhispeakAuthPlugin::SESSION_QUIZ_QUESTION, $quizQuestionInfo);
}
if (!empty($url)) {
echo '
<script>window.location.href = "'.$url.'";</script>
';
}
}

View File

@@ -0,0 +1,48 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\WhispeakAuth\Controller\AuthenticationController;
use Chamilo\PluginBundle\WhispeakAuth\Controller\EnrollmentController;
require_once __DIR__.'/../../../main/inc/global.inc.php';
$action = isset($_POST['action']) ? $_POST['action'] : 'enrollment';
$isEnrollment = 'enrollment' === $action;
$isAuthentify = 'authentify' === $action;
$isAllowed = false;
if ($isEnrollment) {
api_block_anonymous_users(false);
$controller = new EnrollmentController();
header('Content-Type: application/json');
try {
echo json_encode($controller->ajax());
} catch (Exception $exception) {
echo json_encode(
[
'resultHtml' => Display::return_message($exception->getMessage(), 'error', false),
]
);
}
exit;
}
if ($isAuthentify) {
$controller = new AuthenticationController();
header('Content-Type: application/json');
try {
echo json_encode($controller->ajax());
} catch (Exception $exception) {
echo json_encode(
[
'resultHtml' => Display::return_message($exception->getMessage(), 'error', false),
]
);
}
}

View File

@@ -0,0 +1,191 @@
/* For licensing terms, see /license.txt */
window.RecordAudio = (function () {
var timerInterval = 0,
$txtTimer = null;
function startTimer() {
stopTimer();
$txtTimer = $('#txt-timer');
$txtTimer.text('00:00').css('visibility', 'visible');
var timerData = {
hour: 0,
minute: 0,
second: 0
};
timerInterval = setInterval(function(){
timerData.second++;
if (timerData.second >= 60) {
timerData.second = 0;
timerData.minute++;
}
$txtTimer.text(
function () {
var txtSeconds = timerData.minute < 10 ? '0' + timerData.minute : timerData.minute,
txtMinutes = timerData.second < 10 ? '0' + timerData.second : timerData.second;
return txtSeconds + ':' + txtMinutes;
}
);
}, 1000);
}
function stopTimer() {
if (timerInterval) {
clearInterval(timerInterval);
}
if ($txtTimer) {
$txtTimer.css('visibility', 'hidden');
}
}
function useRecordRTC(rtcInfo) {
$(rtcInfo.blockId).show();
var mediaConstraints = {audio: true},
localStream = null,
recordRTC = null,
btnStart = $(rtcInfo.btnStartId),
btnStop = $(rtcInfo.btnStopId),
tagAudio = $(rtcInfo.plyrPreviewId);
function saveAudio() {
var recordedBlob = recordRTC.getBlob();
if (!recordedBlob) {
return;
}
var btnStopText = btnStop.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).text(btnStop.data('loadingtext'));
}
}).done(function (response) {
if (response.text) {
$('#txt-sample-text').text(response.text);
}
$('#messages-deck').html(response.resultHtml);
if ($('#messages-deck > .alert.alert-success').length > 0) {
tagAudio.parents('#audio-wrapper').addClass('hidden').removeClass('show');
} else {
tagAudio.parents('#audio-wrapper').removeClass('hidden').addClass('show');
}
btnStop.prop('disabled', true).html(btnStopText).parent().addClass('hidden');
if ($('#messages-deck > .alert.alert-success').length > 0 ||
$('#messages-deck > .alert.alert-warning [data-reach-attempts]').length > 0
) {
btnStart.prop('disabled', true);
} else {
btnStart.prop('disabled', false);
}
btnStart.parent().removeClass('hidden');
});
}
btnStart.on('click', function () {
tagAudio.prop('src', '');
function successCallback(stream) {
localStream = stream;
recordRTC = RecordRTC(stream, {
recorderType: RecordRTC.StereoAudioRecorder,
type: 'audio',
mimeType: 'audio/wav',
numberOfAudioChannels: 2
});
recordRTC.startRecording();
btnStop.prop('disabled', false).parent().removeClass('hidden');
btnStart.prop('disabled', true).parent().addClass('hidden');
tagAudio.removeClass('show').parents('#audio-wrapper').addClass('hidden');
$('.fa-microphone').addClass('text-danger');
startTimer();
}
function errorCallback(error) {
alert(error);
}
if(!!(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia)) {
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
navigator.getUserMedia(mediaConstraints, successCallback, errorCallback);
return;
}
navigator.mediaDevices.getUserMedia(mediaConstraints)
.then(successCallback)
.catch(errorCallback);
});
btnStop.on('click', function () {
if (!recordRTC) {
return;
}
$('.fa-microphone').removeClass('text-danger');
stopTimer();
recordRTC.stopRecording(function (audioURL) {
tagAudio.prop('src', audioURL);
localStream.getTracks()[0].stop();
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);
}
};
})();

View File

@@ -0,0 +1,17 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\WhispeakAuth\Controller\AuthenticationController;
require_once __DIR__.'/../../main/inc/global.inc.php';
$controller = new AuthenticationController();
try {
$controller->index();
} catch (Exception $exception) {
api_not_allowed(
true,
Display::return_message($exception->getMessage(), 'warning')
);
}

View File

@@ -0,0 +1,79 @@
<?php
/* For licensing terms, see /license.txt */
$cidReset = true;
require_once __DIR__.'/../../main/inc/global.inc.php';
$plugin = WhispeakAuthPlugin::create();
$plugin->protectTool();
$userId = ChamiloSession::read(WhispeakAuthPlugin::SESSION_2FA_USER, 0) ?: api_get_user_id();
/** @var array $lpItemInfo */
$lpItemInfo = ChamiloSession::read(WhispeakAuthPlugin::SESSION_LP_ITEM, []);
/** @var learnpath $oLp */
$oLp = ChamiloSession::read('oLP', null);
/** @var array $lpQuestionInfo */
$lpQuestionInfo = ChamiloSession::read(WhispeakAuthPlugin::SESSION_QUIZ_QUESTION, []);
/** @var Exercise $objExercise */
$objExercise = ChamiloSession::read('objExercise', null);
$isAuthOnLp = !empty($lpItemInfo) && !empty($oLp);
$isAuthOnQuiz = !empty($lpQuestionInfo) && !empty($objExercise);
$showFullPage = !$isAuthOnLp && !$isAuthOnQuiz;
if (empty($userId)) {
api_not_allowed($showFullPage);
}
if (!empty($lpQuestionInfo) && empty($lpItemInfo)) {
echo Display::return_message(
$plugin->get_lang('MaxAttemptsReached').'<br>'
.'<strong>'.$plugin->get_lang('LoginWithUsernameAndPassword').'</strong>',
'warning',
false
);
}
$form = new FormValidator(
'form-login',
'POST',
api_get_path(WEB_PLUGIN_PATH).'whispeakauth/ajax/authentify_password.php',
null,
null,
FormValidator::LAYOUT_BOX_NO_LABEL
);
$form->addElement(
'password',
'password',
get_lang('Pass'),
['id' => 'password', 'icon' => 'lock fa-fw', 'placeholder' => get_lang('Pass')]
);
$form->addHidden('sec_token', '');
$form->setConstants(['sec_token' => Security::get_token()]);
$form->addButton('submitAuth', get_lang('LoginEnter'), 'check', 'primary', 'default', 'btn-block');
$template = new Template(
!$showFullPage ? '' : $plugin->get_title(),
$showFullPage,
$showFullPage,
false,
true,
false
);
$template->assign('form', $form->returnForm());
$content = $template->fetch('whispeakauth/view/authentify_password.html.twig');
if (!empty($lpQuestionInfo) && empty($lpItemInfo)) {
echo $content;
exit;
}
$template->assign('header', $plugin->get_title());
$template->assign('content', $content);
$template->display_one_col_template();

View File

@@ -0,0 +1,21 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\WhispeakAuth\Controller\EnrollmentController;
$cidReset = true;
require_once __DIR__.'/../../main/inc/global.inc.php';
api_block_anonymous_users(true);
$controller = new EnrollmentController();
try {
$controller->index();
} catch (Exception $exception) {
api_not_allowed(
true,
Display::return_message($exception->getMessage(), 'warning')
);
}

View File

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

View File

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

View File

@@ -0,0 +1,48 @@
<?php
/* For licensing terms, see /license.txt */
$strings['plugin_title'] = 'Speech authentication with Whispeak';
$strings['plugin_comment'] = 'Allow speech authentication in Chamilo, which can integrate to the login or inside tests.';
$strings['enable'] = 'Enable';
$strings['enable_help'] = 'Add <code>$_configuration[\'whispeak_auth_enabled\'] = true;</code> in the <code>configuration.php</code> file';
$strings['api_url'] = 'API URL';
$strings['api_url_help'] = 'http://api.whispeak.io:8080/v1/';
$strings['token'] = 'API key';
$strings['max_attempts'] = 'Max attempts';
$strings['max_attempts_help'] = '(Optional) If the Whispeak authentication is failed x times, then ask and verify the password of the user';
$strings['2fa'] = 'Two-Factor Authentication';
$strings['2fa_help'] = 'Allows extend the login page with a Two-Factor Authentication process. After the classic login, the user must authenticate through Whispeak.';
$strings['ActionRegistryPerUser'] = 'Action registry per user';
$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'] = 'Allow audio recording and then read this sentence out loud:';
$strings['SpeechAuthAlreadyEnrolled'] = 'Speech authentication already enrolled previously.';
$strings['SpeechAuthNotEnrolled'] = 'Speech authentication not 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';
$strings['MaxAttemptsReached'] = 'You reached the maximum number of attempts allowed.';
$strings['LoginWithUsernameAndPassword'] = 'You should login using the username and password.';
$strings['YouNeedToIdentifyYourselfToAnswerThisQuestion'] = 'You need to identify yourself to answer this question.';
$strings['IdentifyMe'] = 'Identify me';
$strings['PleaseWaitWhileLoading'] = "Please wait while loading...";
$strings['Quality'] = 'Quality';
$strings['Failed'] = "Failed";
$strings['ActivityId'] = "Activity ID";
$strings['Success'] = "Success";
$strings['MarkForSpeechAuthentication'] = 'Mark it for speech authentication';
$strings['EnrollmentTitle'] = "Enrollment to generate voice print with Whispeak";
$strings['Revocation'] = "Revocation";
$strings['DeleteEnrollments'] = "Delete enrollments";
$strings['NoEnrollment'] = "No enrollment.";
$strings['EnrollmentDeleted'] = "Enrollment deleted";

View File

@@ -0,0 +1,48 @@
<?php
/* For licensing terms, see /license.txt */
$strings['plugin_title'] = 'Authentification vocale avec Whispeak';
$strings['plugin_comment'] = 'Autoriser l\'authentification de la voix dans Chamilo.';
$strings['enable'] = 'Activer';
$strings['enable_help'] = '<p>Ajoutez <code>$_configuration[\'whispeak_auth_enabled\'] = true;</code> dans le fichier <code>configuration.php</code></p>';
$strings['api_url'] = 'URL de l\'API';
$strings['api_url_help'] = 'http://api.whispeak.io:8080/v1.1/';
$strings['token'] = 'Clef API';
$strings['max_attempts'] = 'Tentatives maximum';
$strings['max_attempts_help'] = '(Optionnel) Si l\'authentification Whispeak échoue x fois, alors abandonner et demander le mot de passe de l\'utilisateur';
$strings['2fa'] = 'Authentification à 2 facteurs (2FA)';
$strings['2fa_help'] = 'Autoriser l\'extension du formulaire de login par une page d\'authentification forte. Après le login classique, l\'utilisateur/trice devra aussi s\'authentifier au travers de Whispeak.';
$strings['ActionRegistryPerUser'] = 'Registre d\'actions par utilisateur';
$strings['EnrollmentSampleText'] = 'Le fameux chef-d\'oeuvre Mona Lisa a été peint par Léonardo da Vinci.';
$strings['AuthentifySampleText1'] = 'Tomber comme des mouches.';
$strings['AuthentifySampleText2'] = 'Restez vigilants.';
$strings['AuthentifySampleText3'] = 'Le renard hurle à minuit.';
$strings['AuthentifySampleText4'] = 'Errer dans la campagne.';
$strings['AuthentifySampleText5'] = 'Sous l\'océan.';
$strings['AuthentifySampleText6'] = 'Prendre la mouche.';
$strings['RepeatThisPhrase'] = 'Autorisez l\'enregistrement audio puis lisez cette phrase à voix haute :';
$strings['SpeechAuthAlreadyEnrolled'] = 'L\'authentification de voix a déjà réussi précédemment.';
$strings['SpeechAuthNotEnrolled'] = 'L\'authentification de voix n\'a pas encore été enregistrée.';
$strings['SpeechAuthentication'] = 'Authentification par la 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';
$strings['MaxAttemptsReached'] = 'Vous avez atteint le nombre maximum de tentatives autorisées.';
$strings['LoginWithUsernameAndPassword'] = 'Authentifiez-vous en utilisant votre mot de passe.';
$strings['YouNeedToIdentifyYourselfToAnswerThisQuestion'] = 'Vous devez vous authentifier pour répondre à cette question.';
$strings['IdentifyMe'] = 'M\'identifier';
$strings['PleaseWaitWhileLoading'] = 'Connexion au serveur d\'authentification. Veuillez patienter...';
$strings['Quality'] = 'Quality';
$strings['Failed'] = "Failed";
$strings['ActivityId'] = "Activity ID";
$strings['Success'] = "Success";
$strings['MarkForSpeechAuthentication'] = 'Cocher pour l\'authentification par la voix';
$strings['EnrollmentTitle'] = "Enrôlement pour générer l'empreinte vocale avec Whispeak";
$strings['Revocation'] = "Révocation";
$strings['DeleteEnrollments'] = "Supprimer les inscriptions";
$strings['NoEnrollment'] = "Aucune inscription";
$strings['EnrollmentDeleted'] = "Inscription supprimée.";

View File

@@ -0,0 +1,48 @@
<?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['enable_help'] = '<p>Agrega <code>$_configuration[\'whispeak_auth_enabled\'] = true;</code> al archivo <code>configuration.php</code></p>';
$strings['api_url'] = 'URL del API';
$strings['api_url_help'] = 'http://api.whispeak.io:8080/v1/';
$strings['token'] = 'Llave del API';
$strings['max_attempts'] = 'Máximo de intentos';
$strings['max_attempts_help'] = '(Opcional) Si la autenticación de Whispeak falla x intentos, preguntar y verificar la contraseña del usuario';
$strings['2fa'] = 'Autenticación en dos factores';
$strings['2fa_help'] = 'Permite extender la página de inicio de sesión con un proceso de dos factores. Después del inicio de sesión clásico, el usuario deberá autenticarse a través de Whispeak.';
$strings['ActionRegistryPerUser'] = 'Registro de acciones por usuario';
$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'] = 'Permita la grabación de audio y luego lea esta oración en voz alta:';
$strings['SpeechAuthAlreadyEnrolled'] = 'Autenticación de voz registrada anteriormente.';
$strings['SpeechAuthNotEnrolled'] = 'Autenticación de voz no registrada previamente.';
$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.';
$strings['MaxAttemptsReached'] = 'Ha alcanzado el número máximo de intentos permitidos.';
$strings['LoginWithUsernameAndPassword'] = 'Debe iniciar sesión usando su nombre de usuario y contraseña.';
$strings['YouNeedToIdentifyYourselfToAnswerThisQuestion'] = 'Necesita identificarse para responder esta pregunta.';
$strings['IdentifyMe'] = 'Identificarme';
$strings['PleaseWaitWhileLoading'] = "Por favor, espere mientras dure la carga...";
$strings['Quality'] = 'Calidad';
$strings['Failed'] = "Fallido";
$strings['ActivityId'] = "Identificador de actividad";
$strings['Success'] = "Satisfactotio";
$strings['MarkForSpeechAuthentication'] = 'Marcarlo para autenticación con voz';
$strings['EnrollmentTitle'] = "Inscripción para generar huella de voz con Whispeak";
$strings['Revocation'] = "Revocación";
$strings['DeleteEnrollments'] = "Eliminar inscripciones";
$strings['NoEnrollment'] = "Sin inscripción.";
$strings['EnrollmentDeleted'] = "Inscripción anulada.";

View File

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

View File

@@ -0,0 +1,94 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\WhispeakAuth\Request\ApiRequest;
$cidReset = true;
require_once __DIR__.'/../../main/inc/global.inc.php';
$plugin = WhispeakAuthPlugin::create();
api_protect_admin_script(true);
$plugin->protectTool();
$pageContent = '';
$form = new FormValidator('frm_revocation', 'GET');
$form->setAttribute('onsubmit', "return confirm('".addslashes(get_lang('AreYouSureToDelete'))."');");
$slctUsers = $form->addSelectAjax(
'users',
get_lang('Users'),
[],
[
'url' => api_get_path(WEB_AJAX_PATH).'user_manager.ajax.php?a=get_user_like',
'id' => 'user_id',
'multiple' => true,
]
);
$form->addButton('asubmit', $plugin->get_lang('DeleteEnrollments'), 'times', 'danger');
$form->addRule('users', get_lang('ThisFieldIsRequired'), 'required');
$userIds = [];
if ($form->validate()) {
$formValues = $form->exportValues();
$userIds = $formValues['users'] ?: [];
/** @var int $userId */
foreach ($userIds as $userId) {
$user = api_get_user_entity($userId);
if (null === $user) {
continue;
}
$slctUsers->addOption($user->getCompleteNameWithUsername(), $user->getId());
$request = new ApiRequest();
$pageContent .= Display::page_subheader($user->getCompleteNameWithUsername(), null, 'h4');
try {
$request->deleteEnrollment($user);
$response = WhispeakAuthPlugin::deleteEnrollment($user->getId());
$pageContent .= Display::return_message(
$plugin->get_lang('EnrollmentDeleted'),
'success'
);
} catch (Exception $e) {
$pageContent .= Display::return_message(
$e->getMessage(),
'error'
);
}
}
}
$interbreadcrumb[] = [
'name' => get_lang('Administration'),
'url' => api_get_path(WEB_CODE_PATH).'admin/index.php',
];
$interbreadcrumb[] = [
'name' => $plugin->get_title(),
'url' => 'admin.php',
];
$actionsLeft = Display::url(
Display::return_icon('back.png', $plugin->get_lang('Back'), [], ICON_SIZE_MEDIUM),
'admin.php'
);
$pageTitle = $plugin->get_lang('Revocation');
$template = new Template($pageTitle);
$template->assign('actions', Display::toolbarAction('whispeak_admin', [$actionsLeft]));
$template->assign('header', $pageTitle);
$template->assign(
'content',
$form->returnForm().PHP_EOL.$pageContent
);
$template->display_one_col_template();

View File

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

View File

@@ -0,0 +1,44 @@
<div class="row">
<div class="col-sm-6 col-sm-offset-3 col-md-4 col-md-offset-4">
{{ form }}
<br>
<div id="frm-login-result"></div>
</div>
</div>
<script>
$(function () {
$('#form-login').on('submit', function (e) {
e.preventDefault();
var formData = new FormData(this),
self = $(this);
self.children().prop('disabled', true);
self.find(':submit em.fa').get(0).className = 'fa fa-spinner fa-spin';
$
.ajax({
type: 'POST',
url: this.action,
data: formData,
processData: false,
contentType: false
})
.then(function (response) {
$('#frm-login-result').html(response);
self.children().prop('disabled', false);
self.find(':submit em.fa').get(0).className = 'fa fa-check';
if ($('#frm-login-result > .alert.alert-success').length > 0 ||
$('#frm-login-result > .alert.alert-danger [data-reach-attempts]').length > 0
) {
self.find(':submit').prop('disabled', true);
}
});
this.reset();
});
});
</script>

View File

@@ -0,0 +1,44 @@
{% extends 'whispeakauth/view/record_audio.html.twig' %}
{% block intro %}
{% if show_form %}
<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>
{% endif %}
{{ parent() }}
{% endblock %}
{% block config_data %}
{% if show_form %}
$('#username').on('change', function () {
$('#record-audio-recordrtc, #btn-start-record, #btn-stop-record, #btn-save-record').off('click', '');
{% endif %}
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()
}
}
);
{% if show_form %}
});
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,45 @@
<div class="text-center">
<p>{{ 'YouNeedToIdentifyYourselfToAnswerThisQuestion'|get_plugin_lang('WhispeakAuthPlugin') }}</p>
<button type="button" class="btn btn-info" id="whispeak-question-{{ question }}" data-loading-text="{{ 'PleaseWaitWhileLoading'|get_plugin_lang('WhispeakAuthPlugin')|escape('html') }}">
{{ 'IdentifyMe'|get_plugin_lang('WhispeakAuthPlugin') }}
</button>
</div>
<script>
$(function () {
function loadAuth() {
var $btnTrigger = $('#whispeak-question-{{ question }}'),
originalText = $btnTrigger.text(),
$modal = $('#global-modal'),
$modalTitle = $modal.find('.modal-title'),
$modalBody = $modal.find('.modal-body'),
$originalLoadingText = $btnTrigger.data('loading-text');
$btnTrigger.text($originalLoadingText).prop('disabled', true);
$modalTitle.text($originalLoadingText);
$modalBody.html('');
$modal.modal('show');
$
.ajax({
url: _p.web_plugin + 'whispeakauth/authentify.php'
})
.then(function (response) {
$modalBody.html(response);
$modalTitle.text('{{ 'plugin_title'|get_plugin_lang('WhispeakAuthPlugin') }}');
$btnTrigger.text(originalText).prop('disabled', false);
});
}
$('#whispeak-question-{{ question }}').on('click', function (e) {
e.preventDefault();
loadAuth();
});
loadAuth();
});
</script>

View File

@@ -0,0 +1,59 @@
<div id="record-audio-recordrtc" class="row">
<div class="col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-0">
{% 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 id="txt-timer" class="text-danger h3 show"></span>
</div>
<div class="col-sm-9 text-center">
<p class="lead" id="txt-sample-text">{{ sample_text }}</p>
</div>
</div>
</div>
{% endblock %}
<div class="text-center">
<p>
<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>
</p>
<p class="hidden">
<button class="btn btn-danger" type="button" id="btn-stop-record" disabled
data-loadingtext="{{ 'Uploading'|get_lang }}">
<span class="fa fa-square fa-fw" aria-hidden="true"></span> {{ 'StopRecordingAudio'|get_lang }}
</button>
</p>
</div>
</div>
<div class="col-sm-8 col-sm-offset-2 col-md-5 col-md-offset-1">
<hr class="visible-sm">
<div id="messages-deck"></div>
<div class="hidden" id="audio-wrapper">
<p>
<audio class="center-block" controls id="record-preview"></audio>
</p>
</div>
</div>
</div>
<script>
$(function () {
{% block config_data %}
var data = {action: 'enrollment', license: 0 };
RecordAudio.init(
{
blockId: '#record-audio-recordrtc',
btnStartId: '#btn-start-record',
btnStopId: '#btn-stop-record',
plyrPreviewId: '#record-preview',
data: data
}
);
{% endblock %}
});
</script>