Chamilo is a learning management system focused on ease of use and accessibility
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
chamilo-lms/plugin/whispeakauth/WhispeakAuthPlugin.php

846 lines
23 KiB

<?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 Symfony\Component\Filesystem\Filesystem;
/**
* Class WhispeakAuthPlugin.
*/
class WhispeakAuthPlugin extends Plugin implements 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 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.
*/
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();
}
public function install()
{
$this->installExtraFields();
$this->installEntities();
$this->installHook();
}
public function uninstall()
{
$this->uninstallHook();
$this->uninstallExtraFields();
$this->uninstallEntities();
}
/**
* @return string
*/
public function getEntityPath()
{
return api_get_path(SYS_PATH).'src/Chamilo/PluginBundle/Entity/'.$this->getCamelCaseName();
}
/**
* @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
*
* @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;
}
$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.
*/
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')
);
return;
}
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);
}
$table = Database::get_main_table('whispeak_log_event');
$sql = "DROP TABLE IF EXISTS $table";
Database::query($sql);
}
}