Internal: Add plugin/whispeakauth as is from 1.11.x

pull/3466/head
Julio Montoya 6 years ago
parent 6bc39a7354
commit 1ab4d4afd1
  1. 339
      public/plugin/whispeakauth/Controller/AuthenticationController.php
  2. 67
      public/plugin/whispeakauth/Controller/BaseController.php
  3. 99
      public/plugin/whispeakauth/Controller/EnrollmentController.php
  4. 145
      public/plugin/whispeakauth/Entity/LogEvent.php
  5. 84
      public/plugin/whispeakauth/Entity/LogEventLp.php
  6. 84
      public/plugin/whispeakauth/Entity/LogEventQuiz.php
  7. 12
      public/plugin/whispeakauth/README.md
  8. 194
      public/plugin/whispeakauth/Request/ApiRequest.php
  9. 784
      public/plugin/whispeakauth/WhispeakAuthPlugin.php
  10. 59
      public/plugin/whispeakauth/WhispeakConditionalLoginHook.php
  11. 59
      public/plugin/whispeakauth/WhispeakMyStudentsLpTrackingHook.php
  12. 77
      public/plugin/whispeakauth/WhispeakMyStudentsQuizTrackingHook.php
  13. 128
      public/plugin/whispeakauth/admin.php
  14. 140
      public/plugin/whispeakauth/ajax/authentify_password.php
  15. 127
      public/plugin/whispeakauth/ajax/record_audio.php
  16. 53
      public/plugin/whispeakauth/assets/js/RecordAudio.js
  17. 30
      public/plugin/whispeakauth/authentify.php
  18. 79
      public/plugin/whispeakauth/authentify_password.php
  19. 30
      public/plugin/whispeakauth/enrollment.php
  20. 1
      public/plugin/whispeakauth/index.php
  21. 1
      public/plugin/whispeakauth/install.php
  22. 27
      public/plugin/whispeakauth/lang/english.php
  23. 40
      public/plugin/whispeakauth/lang/french.php
  24. 27
      public/plugin/whispeakauth/lang/spanish.php
  25. 1
      public/plugin/whispeakauth/plugin.php
  26. 1
      public/plugin/whispeakauth/uninstall.php
  27. 44
      public/plugin/whispeakauth/view/authentify_password.html.twig
  28. 34
      public/plugin/whispeakauth/view/authentify_recorder.html.twig
  29. 45
      public/plugin/whispeakauth/view/quiz_question.html.twig
  30. 49
      public/plugin/whispeakauth/view/record_audio.html.twig

@ -0,0 +1,339 @@
<?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);
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);
$request = new ApiRequest();
$success = $request->performAuthentication($token, $user, $audioFilePath);
\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, []);
$message = $this->plugin->get_lang('AuthentifySuccess');
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());
}
$message = $this->plugin->get_lang('AuthentifyFailed');
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
.'<br><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 .= '<br>'
.Display::url(
get_lang('LostPassword'),
api_get_path(WEB_CODE_PATH).'auth/lostPassword.php',
['target' => $lpItemInfo ? '_top' : '_self']
);
}
}
}
echo Display::return_message(
$message,
$success ? 'success' : 'warning',
false
);
if (!$success && $maxAttempts && $failedLogins >= $maxAttempts) {
ChamiloSession::erase(WhispeakAuthPlugin::SESSION_FAILED_LOGINS);
if (!empty($lpItemInfo)) {
echo '<script>window.location.href = "'
.api_get_path(WEB_PLUGIN_PATH)
.'whispeakauth/authentify_password.php";</script>';
exit;
}
if (!empty($quizQuestionInfo)) {
$url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit.php?'.$quizQuestionInfo['url_params'];
ChamiloSession::write(WhispeakAuthPlugin::SESSION_AUTH_PASSWORD, true);
echo "<script>window.location.href = '".$url."';</script>";
exit;
}
echo '<script>window.location.href = "'.api_get_path(WEB_PATH).'";</script>';
exit;
}
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']
);
echo '<script>window.location.href = "'.$lpItemInfo['src'].'";</script>';
exit;
}
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']
);
echo '<script>window.location.href = "'.$url.'";</script>';
exit;
}
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);
echo '<script>window.location.href = "'.api_get_path(WEB_PATH).'";</script>';
}
}
/**
* {@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);
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();
}
}

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

@ -0,0 +1,99 @@
<?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()
{
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();
$response = $request->createEnrollment($token, $audioFilePath, $user);
\ChamiloSession::erase(\WhispeakAuthPlugin::SESSION_SENTENCE_TEXT);
$this->plugin->saveEnrollment($user, $response['speaker']);
echo \Display::return_message($this->plugin->get_lang('EnrollmentSuccess'), 'success');
}
/**
* {@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();
}
}

@ -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
{
const STATUS_FAILED = 0;
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 '-';
}
}

@ -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";
}
}

@ -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";
}
}

@ -1,10 +1,20 @@
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 HTTP, then Whispeak authentication may not work.**
Instructions:
-------------
> Make sure the directory `src/Chamilo\PluginBundle` is writable by the web server in order for the plugin is installed
> properly. This might imply a manual change on your server (outside of the Chamilo interface).
1. Install plugin in Chamilo.
2. Set the plugin configuration with the token and API url. And enable the plugin.
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.

@ -0,0 +1,194 @@
<?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
);
}
/**
* @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);
try {
$this->sendRequest(
'post',
'auth',
$token,
$langIso,
[
[
'name' => 'speaker',
'contents' => $wsid->getValue(),
],
[
'name' => 'file',
'contents' => fopen($audioFilePath, 'r'),
'filename' => basename($audioFilePath),
],
]
);
return true;
} catch (\Exception $e) {
return false;
}
}
/**
* @param string $method
* @param string $uri
* @param string $authBearer
* @param string $lang
*
* @throws \Exception
*
* @return array
*/
private function sendRequest($method, $uri, $authBearer, $lang, array $multipart = [])
{
$httpClient = new Client(['base_uri' => $this->plugin->getApiUrl()]);
try {
$responseBody = $httpClient
->request(
$method,
$uri,
[
'headers' => [
'Authorization' => "Bearer $authBearer",
'Accept-Language' => $lang,
],
'multipart' => $multipart,
]
)
->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);
if (empty($json['message'])) {
throw new \Exception($requestException->getMessage());
}
$message = is_array($json['message']) ? implode(PHP_EOL, $json['message']) : $json['message'];
throw new \Exception($message);
} catch (Exception $exception) {
throw new \Exception($exception->getMessage());
}
}
}

@ -1,22 +1,35 @@
<?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\CoreBundle\Entity\User;
use Symfony\Component\Filesystem\Filesystem;
/**
* Class WhispeakAuthPlugin.
*/
class WhispeakAuthPlugin extends Plugin
class WhispeakAuthPlugin extends Plugin implements \Chamilo\CoreBundle\Hook\Interfaces\HookPluginInterface
{
const SETTING_ENABLE = 'enable';
const SETTING_MAX_ATTEMPTS = 'max_attempts';
const SETTING_2FA = '2fa';
const SETTING_API_URL = 'api_url';
const SETTING_TOKEN = 'token';
const SETTING_INSTRUCTION = 'instruction';
const EXTRAFIELD_AUTH_UID = 'whispeak_auth_uid';
const EXTRAFIELD_LP_ITEM = 'whispeak_lp_item';
const EXTRAFIELD_QUIZ_QUESTION = 'whispeak_quiz_question';
const SESSION_FAILED_LOGINS = 'whispeak_failed_logins';
const SESSION_2FA_USER = 'whispeak_user_id';
const SESSION_LP_ITEM = 'whispeak_lp_item';
const SESSION_QUIZ_QUESTION = 'whispeak_quiz_question';
const SESSION_AUTH_PASSWORD = 'whispeak_auth_password';
const SESSION_SENTENCE_TEXT = 'whispeak_sentence_text';
/**
* StudentFollowUpPlugin constructor.
@ -30,11 +43,24 @@ class WhispeakAuthPlugin extends Plugin
self::SETTING_ENABLE => 'boolean',
self::SETTING_API_URL => 'text',
self::SETTING_TOKEN => 'text',
self::SETTING_INSTRUCTION => 'html',
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
*/
@ -42,34 +68,29 @@ class WhispeakAuthPlugin extends Plugin
{
static $result = null;
return $result ?: $result = new self();
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'),
''
);
$this->installExtraFields();
$this->installEntities();
$this->installHook();
}
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]);
$this->uninstallHook();
$this->uninstallExtraFields();
$this->uninstallEntities();
}
$em->remove($extraField);
$em->flush();
/**
* @return string
*/
public function getEntityPath()
{
return api_get_path(SYS_PATH).'src/Chamilo/PluginBundle/Entity/'.$this->getCamelCaseName();
}
/**
@ -81,12 +102,50 @@ class WhispeakAuthPlugin extends Plugin
$efRepo = $em->getRepository('ChamiloCoreBundle:ExtraField');
/** @var ExtraField $extraField */
return $efRepo->findOneBy(
$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;
}
/**
@ -101,7 +160,101 @@ class WhispeakAuthPlugin extends Plugin
$efvRepo = $em->getRepository('ChamiloCoreBundle:ExtraFieldValues');
/** @var ExtraFieldValues $value */
return $efvRepo->findOneBy(['field' => $extraField, 'itemId' => $userId]);
$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;
}
/**
@ -129,158 +282,565 @@ class WhispeakAuthPlugin extends Plugin
}
/**
* @param string $filePath
* @param string $uid
*
* @return array
* @throws \Doctrine\ORM\OptimisticLockException
*/
public function requestEnrollment(User $user, $filePath)
public function saveEnrollment(User $user, $uid)
{
$metadata = [
'motherTongue' => $user->getLanguage(),
'spokenTongue' => $user->getLanguage(),
'audioType' => 'pcm',
];
$em = Database::getManager();
$extraFieldValue = self::getAuthUidValue($user->getId());
return $this->sendRequest(
'enrollment',
$metadata,
$user,
$filePath
);
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();
}
/**
* @param string $uid
* @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 function saveEnrollment(User $user, $uid)
public static function deleteEnrollment($userId)
{
$extraFieldValue = self::getAuthUidValue($userId);
if (empty($extraFieldValue)) {
return false;
}
$em = Database::getManager();
$value = self::getAuthUidValue($user->getId());
$em->remove($extraFieldValue);
$em->flush();
if (empty($value)) {
$ef = self::getAuthUidExtraField();
$now = new DateTime('now', new DateTimeZone('UTC'));
return true;
}
$value = new ExtraFieldValues();
$value
->setField($ef)
->setItemId($user->getId())
->setUpdatedAt($now);
/**
* Check if the WhispeakAuth plugin is installed and enabled.
*
* @param bool $checkEnabled Check if, additionnally to being installed, the plugin is enabled
*
* @return bool
*/
public function isEnabled($checkEnabled = false)
{
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;
}
$value->setValue($uid);
$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;
}
$em->persist($value);
$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;
}
public function requestAuthentify(User $user, $filePath)
/**
* @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)
{
$value = self::getAuthUidValue($user->getId());
$em = Database::getManager();
if (empty($value)) {
$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;
}
$metadata = [
'uid' => $value->getValue(),
'audioType' => 'pcm',
];
$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 $this->sendRequest(
'authentify',
$metadata,
$user,
$filePath
);
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 function getAuthentifySampleText()
public static function countAllAttemptsInLearningPath($lpId, $userId)
{
$phrases = [];
$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]);
for ($i = 1; $i <= 6; $i++) {
$phrases[] = $this->get_lang("AuthentifySampleText$i");
}
$totalCount = (int) $query->getSingleScalarResult();
$rand = array_rand($phrases, 1);
return $totalCount;
}
return $phrases[$rand];
/**
* @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;
}
/**
* @return bool
* @param int $quizId
* @param int $userId
*
* @throws \Doctrine\ORM\Query\QueryException
*
* @return string
*/
public function toolIsEnabled()
public static function countAllAttemptsInQuiz($quizId, $userId)
{
return 'true' === $this->get(self::SETTING_ENABLE);
$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;
}
/**
* Access not allowed when tool is not enabled.
* @param int $quizId
* @param int $userId
*
* @param bool $printHeaders Optional. Print headers.
* @throws \Doctrine\ORM\Query\QueryException
*
* @return string
*/
public function protectTool($printHeaders = true)
public static function countSuccessAttemptsInQuiz($quizId, $userId)
{
if ($this->toolIsEnabled()) {
return;
}
$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]);
api_not_allowed($printHeaders);
$totalCount = (int) $query->getSingleScalarResult();
return $totalCount;
}
/**
* @return string
*/
private function getApiUrl()
public function getApiUrl()
{
$url = $this->get(self::SETTING_API_URL);
return trim($url, " \t\n\r \v/");
return trim($url, " \t\n\r \v/").'/';
}
/**
* @param string $endPoint
* @param string $filePath
*
* @return array
* Install extra fields for user, learning path and quiz question.
*/
private function sendRequest($endPoint, array $metadata, User $user, $filePath)
private function installExtraFields()
{
$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),
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,
];
$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);
$extraField->save($params);
}
/**
* Install the Doctrine's entities.
*/
private function installEntities()
{
$pluginEntityPath = $this->getEntityPath();
if (!is_dir($pluginEntityPath)) {
if (!is_writable(dirname($pluginEntityPath))) {
Display::addFlash(
Display::return_message(get_lang('ErrorCreatingDir').": $pluginEntityPath", 'error')
);
$result = json_decode($result, true);
return;
}
if (!empty($result['error'])) {
return null;
mkdir($pluginEntityPath, api_get_permissions_for_new_directories());
}
$fs = new Filesystem();
$fs->mirror(__DIR__.'/Entity/', $pluginEntityPath, null, ['override']);
$schema = Database::getManager()->getConnection()->getSchemaManager();
if (false === $schema->tablesExist('whispeak_log_event')) {
$sql = "CREATE TABLE whispeak_log_event (
id INT AUTO_INCREMENT NOT NULL,
user_id INT NOT NULL,
lp_item_id INT DEFAULT NULL,
lp_id INT DEFAULT NULL,
question_id INT DEFAULT NULL,
quiz_id INT DEFAULT NULL,
datetime DATETIME NOT NULL,
action_status SMALLINT NOT NULL,
discr VARCHAR(255) NOT NULL,
INDEX IDX_A5C4B9FFA76ED395 (user_id),
INDEX IDX_A5C4B9FFDBF72317 (lp_item_id),
INDEX IDX_A5C4B9FF68DFD1EF (lp_id),
INDEX IDX_A5C4B9FF1E27F6BF (question_id),
INDEX IDX_A5C4B9FF853CD175 (quiz_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB";
Database::query($sql);
$sql = "ALTER TABLE whispeak_log_event ADD CONSTRAINT FK_A5C4B9FFA76ED395
FOREIGN KEY (user_id) REFERENCES user (id)";
Database::query($sql);
$sql = "ALTER TABLE whispeak_log_event ADD CONSTRAINT FK_A5C4B9FFDBF72317
FOREIGN KEY (lp_item_id) REFERENCES c_lp_item (iid)";
Database::query($sql);
$sql = "ALTER TABLE whispeak_log_event ADD CONSTRAINT FK_A5C4B9FF68DFD1EF
FOREIGN KEY (lp_id) REFERENCES c_lp (iid)";
Database::query($sql);
$sql = "ALTER TABLE whispeak_log_event ADD CONSTRAINT FK_A5C4B9FF1E27F6BF
FOREIGN KEY (question_id) REFERENCES c_quiz_question (iid)";
Database::query($sql);
$sql = "ALTER TABLE whispeak_log_event ADD CONSTRAINT FK_A5C4B9FF853CD175
FOREIGN KEY (quiz_id) REFERENCES c_quiz (iid)";
Database::query($sql);
}
}
/**
* 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()
{
$pluginEntityPath = $this->getEntityPath();
$fs = new Filesystem();
if ($fs->exists($pluginEntityPath)) {
$fs->remove($pluginEntityPath);
}
return json_decode($result, true);
$table = Database::get_main_table('whispeak_log_event');
$sql = "DROP TABLE IF EXISTS $table";
Database::query($sql);
}
}

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

@ -0,0 +1,59 @@
<?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)
{
return [
'value' => WhispeakAuthPlugin::create()->get_lang('plugin_title'),
'attrs' => ['class' => 'text-center'],
];
}
/**
* @throws \Doctrine\ORM\Query\QueryException
*
* @return array
*/
public function trackingContent(HookMyStudentsLpTrackingEventInterface $hook)
{
$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,
];
}
}

@ -0,0 +1,77 @@
<?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)
{
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)
{
$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,
];
}
}

@ -0,0 +1,128 @@
<?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');
$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'));
$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());
$table = new HTML_Table(['class' => 'table table-hover']);
$table->setHeaderContents(0, 0, get_lang('DateTime'));
$table->setHeaderContents(0, 1, get_lang('Type'));
$table->setHeaderContents(0, 2, get_lang('Status'));
foreach ($logEvents as $i => $logEvent) {
$row = $i + 1;
$type = '';
switch (get_class($logEvent)) {
case LogEventQuiz::class:
$type = '<span class="label label-info">'.get_lang('Question').'</span>'.PHP_EOL;
break;
case LogEventLp::class:
$type = '<span class="label label-info">'.get_lang('LearningPath').'</span>'.PHP_EOL;
break;
}
$table->setCellContents(
$row,
0,
[
api_convert_and_format_date($logEvent->getDatetime(), DATE_TIME_FORMAT_SHORT),
$type.PHP_EOL.$logEvent->getTypeString(),
$logEvent->getActionStatus() === LogEvent::STATUS_SUCCESS ? get_lang('Success') : get_lang('Failed'),
]
);
}
$table->updateColAttributes(0, ['class' => 'text-center']);
$table->updateColAttributes(2, ['class' => 'text-center']);
$pageContent .= Display::page_header($user->getCompleteNameWithUsername());
$pageContent .= $table->toHtml();
}
$template = new Template($plugin->get_title());
$template->assign(
'content',
$form->returnForm().PHP_EOL.$pageContent
);
$template->display_one_col_template();

@ -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::isPasswordValid($user->getPassword(), $password, $user->getSalt());
$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>
';
}
}

@ -1,12 +1,8 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\User;
use FFMpeg\FFMpeg;
use FFMpeg\Format\Audio\Wav;
$cidReset = true;
use Chamilo\PluginBundle\WhispeakAuth\Controller\AuthenticationController;
use Chamilo\PluginBundle\WhispeakAuth\Controller\EnrollmentController;
require_once __DIR__.'/../../../main/inc/global.inc.php';
@ -14,124 +10,29 @@ $action = isset($_POST['action']) ? $_POST['action'] : 'enrollment';
$isEnrollment = 'enrollment' === $action;
$isAuthentify = 'authentify' === $action;
$isAllowed = true;
$isAllowed = false;
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('You are not allowed to see this page. Either your connection has expired or you are trying to access a page for which you do not have the sufficient privileges.'), 'error');
exit;
}
$plugin = WhispeakAuthPlugin::create();
$plugin->protectTool(false);
if ($isAuthentify) {
$em = Database::getManager();
/** @var User|null $user */
$user = $em->getRepository('ChamiloCoreBundle: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('No user'), 'error');
exit;
}
$path = api_upload_file('whispeakauth', $_FILES['audio'], $user->getId());
if (false === $path) {
echo Display::return_message(get_lang('Upload failed, please check maximum file size limits and folder rights.'), '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';
$controller = new EnrollmentController();
try {
$ffmpeg = FFMpeg::create();
$audio = $ffmpeg->open($originFullPath);
$audio->save(new Wav(), $newFullPath);
$controller->ajax();
} catch (Exception $exception) {
echo Display::return_message($exception->getMessage(), 'error');
exit;
WhispeakAuthPlugin::displayNotAllowedMessage(
$exception->getMessage()
);
}
}
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;
die;
}
if ($isAuthentify) {
$result = $plugin->requestAuthentify($user, $newFullPath);
$controller = new AuthenticationController();
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('Try again'), 'warning');
exit;
try {
$controller->ajax();
} catch (Exception $exception) {
echo Display::return_message($exception->getMessage(), 'error', false);
}
$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;
}

@ -9,7 +9,6 @@ window.RecordAudio = (function () {
recordRTC = null,
btnStart = $(rtcInfo.btnStartId),
btnStop = $(rtcInfo.btnStopId),
btnSave = $(rtcInfo.btnSaveId),
tagAudio = $(rtcInfo.plyrPreviewId);
function saveAudio() {
@ -19,7 +18,7 @@ window.RecordAudio = (function () {
return;
}
var btnSaveText = btnSave.html();
var btnStopText = btnStop.html();
var fileExtension = recordedBlob.type.split('/')[1];
var formData = new FormData();
@ -41,15 +40,28 @@ window.RecordAudio = (function () {
type: 'POST',
beforeSend: function () {
btnStart.prop('disabled', true);
btnStop.prop('disabled', true);
btnSave.prop('disabled', true).text(btnSave.data('loadingtext'));
btnStop.prop('disabled', true).text(btnStop.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');
$('#messages-deck').html(response);
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');
});
}
@ -66,7 +78,6 @@ window.RecordAudio = (function () {
});
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');
@ -97,26 +108,12 @@ window.RecordAudio = (function () {
}
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');
tagAudio.prop('src', audioURL);
localStream.getTracks()[0].stop();
});
});
btnSave.on('click', function () {
if (!recordRTC) {
return;
}
saveAudio();
saveAudio();
});
});
}
@ -135,5 +132,5 @@ window.RecordAudio = (function () {
useRecordRTC(rtcInfo);
}
}
};
})();

@ -1,27 +1,17 @@
<?php
/* For licensing terms, see /license.txt */
$cidReset = true;
use Chamilo\PluginBundle\WhispeakAuth\Controller\AuthenticationController;
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');
$controller = new AuthenticationController();
$template->assign('header', $plugin->get_title());
$template->assign('content', $content);
$template->display_one_col_template();
try {
$controller->index();
} catch (Exception $exception) {
api_not_allowed(
true,
Display::return_message($exception->getMessage(), 'warning')
);
}

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

@ -1,29 +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);
$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');
$controller = new EnrollmentController();
$template->assign('header', $plugin->get_title());
$template->assign('content', $content);
$template->display_one_col_template();
try {
$controller->index();
} catch (Exception $exception) {
api_not_allowed(
true,
Display::return_message($exception->getMessage(), 'warning')
);
}

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

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

@ -1,16 +1,18 @@
<?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['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['instruction'] = '<p>Add <code>$_configuration[\'whispeak_auth_enabled\'] = true;</code>'.
'in the <code>configuration.php</code> file</p>';
$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['EnrollmentSampleText'] = 'The famous Mona Lisa painting was painted by Leonardo Da Vinci.';
$strings['AuthentifySampleText1'] = 'Dropping Like Flies.';
@ -19,16 +21,23 @@ $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['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";

@ -1,19 +1,43 @@
<?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['EnrollmentSampleText'] = 'Le fameux chef-d\'oeuvre Mona Lisa a été peint par Léonardo da Vinci.';
$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['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['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";

@ -1,16 +1,18 @@
<?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['instruction'] = '<p>Agrega <code>$_configuration[\'whispeak_auth_enabled\'] = true;</code>'.
'al archivo <code>configuration.php</code></p>';
$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['EnrollmentSampleText'] = 'El famoso cuadro de Mona Lisa fue pintado por Leonardo Da Vinci.';
$strings['AuthentifySampleText1'] = 'Cayendo como moscas.';
@ -19,16 +21,23 @@ $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['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";

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

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

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

@ -1,26 +1,29 @@
{% 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">
{% 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>
</div>
</form>
</form>
<hr>
<hr>
{% endif %}
{{ parent() }}
{% endblock %}
{% block config_data %}
$('#username').on('change', function () {
$('#record-audio-recordrtc, #btn-start-record, #btn-stop-record, #btn-save-record').off('click', '');
{% 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',
@ -34,5 +37,8 @@
}
}
);
});
{% if show_form %}
});
{% endif %}
{% endblock %}

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

@ -1,12 +1,11 @@
<div id="record-audio-recordrtc" class="row">
<div class="col-sm-6">
<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 class="sr-only">{{ 'RecordAudio'|get_lang }}</span>
</div>
<div class="col-sm-9 text-center">
<p class="lead">{{ sample_text }}</p>
@ -15,51 +14,43 @@
</div>
{% endblock %}
<ul class="list-inline text-center">
<li>
<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>
</li>
<li class="hidden">
<button class="btn btn-danger" type="button" id="btn-stop-record" disabled>
</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>
</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>
</p>
</div>
</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 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',
btnSaveId: '#btn-save-record',
plyrPreviewId: '#record-preview',
data: {
action: 'enrollment'
}
data: data
}
);
{% endblock %}

Loading…
Cancel
Save