chamilo/chamilo-lms

View on GitHub
public/plugin/xapi/src/XApiPlugin.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

/* For licensing terms, see /license.txt */

use Chamilo\CoreBundle\Entity\XApiToolLaunch;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver;
use Doctrine\ORM\ORMException;
use GuzzleHttp\RequestOptions;
use Http\Adapter\Guzzle6\Client;
use Http\Message\MessageFactory\GuzzleMessageFactory;
use Symfony\Component\Uid\Uuid;
use Xabbuh\XApi\Client\Api\StatementsApiClientInterface;
use Xabbuh\XApi\Client\XApiClientBuilder;
use Xabbuh\XApi\Model\Agent;
use Xabbuh\XApi\Model\IRI;
use Xabbuh\XApi\Serializer\Symfony\Serializer;

/**
 * Class XApiPlugin.
 */
class XApiPlugin extends Plugin implements HookPluginInterface
{
    public const SETTING_LRS_URL = 'lrs_url';
    public const SETTING_LRS_AUTH_USERNAME = 'lrs_auth_username';
    public const SETTING_LRS_AUTH_PASSWORD = 'lrs_auth_password';
    public const SETTING_CRON_LRS_URL = 'cron_lrs_url';
    public const SETTING_CRON_LRS_AUTH_USERNAME = 'cron_lrs_auth_username';
    public const SETTING_CRON_LRS_AUTH_PASSWORD = 'cron_lrs_auth_password';
    public const SETTING_UUID_NAMESPACE = 'uuid_namespace';
    public const SETTING_LRS_LP_ITEM_ACTIVE = 'lrs_lp_item_viewed_active';
    public const SETTING_LRS_LP_ACTIVE = 'lrs_lp_end_active';
    public const SETTING_LRS_QUIZ_ACTIVE = 'lrs_quiz_active';
    public const SETTING_LRS_QUIZ_QUESTION_ACTIVE = 'lrs_quiz_question_active';
    public const SETTING_LRS_PORTFOLIO_ACTIVE = 'lrs_portfolio_active';

    public const STATE_FIRST_LAUNCH = 'first_launch';
    public const STATE_LAST_LAUNCH = 'last_launch';

    /**
     * XApiPlugin constructor.
     */
    protected function __construct()
    {
        $version = '0.3 (beta)';
        $author = [
            'Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>',
        ];
        $settings = [
            self::SETTING_UUID_NAMESPACE => 'text',

            self::SETTING_LRS_URL => 'text',
            self::SETTING_LRS_AUTH_USERNAME => 'text',
            self::SETTING_LRS_AUTH_PASSWORD => 'text',

            self::SETTING_CRON_LRS_URL => 'text',
            self::SETTING_CRON_LRS_AUTH_USERNAME => 'text',
            self::SETTING_CRON_LRS_AUTH_PASSWORD => 'text',

            self::SETTING_LRS_LP_ITEM_ACTIVE => 'boolean',
            self::SETTING_LRS_LP_ACTIVE => 'boolean',
            self::SETTING_LRS_QUIZ_ACTIVE => 'boolean',
            self::SETTING_LRS_QUIZ_QUESTION_ACTIVE => 'boolean',
            self::SETTING_LRS_PORTFOLIO_ACTIVE => 'boolean',
        ];

        parent::__construct(
            $version,
            implode(', ', $author),
            $settings
        );
    }

    /**
     * @return \XApiPlugin
     */
    public static function create()
    {
        static $result = null;

        return $result ? $result : $result = new self();
    }

    /**
     * Process to install plugin.
     */
    public function install()
    {
        $this->installInitialConfig();
        $this->addCourseTools();
        $this->installHook();
    }

    /**
     * Process to uninstall plugin.
     */
    public function uninstall()
    {
        $this->uninstallHook();
        $this->deleteCourseTools();
    }

    /**
     * {@inheritdoc}
     */
    public function uninstallHook()
    {
        $learningPathItemViewedHook = XApiLearningPathItemViewedHookObserver::create();
        $learningPathEndHook = XApiLearningPathEndHookObserver::create();
        $quizQuestionAnsweredHook = XApiQuizQuestionAnsweredHookObserver::create();
        $quizEndHook = XApiQuizEndHookObserver::create();
        $createCourseHook = XApiCreateCourseHookObserver::create();
        $portfolioItemAddedHook = XApiPortfolioItemAddedHookObserver::create();
        $portfolioItemCommentedHook = XApiPortfolioItemCommentedHookObserver::create();
        $portfolioItemHighlightedHook = XApiPortfolioItemHighlightedHookObserver::create();
        $portfolioDownloaded = XApiPortfolioDownloadedHookObserver::create();
        $portfolioItemScoredHook = XApiPortfolioItemScoredHookObserver::create();
        $portfolioCommentedScoredHook = XApiPortfolioCommentScoredHookObserver::create();
        $portfolioItemEditedHook = XApiPortfolioItemEditedHookObserver::create();
        $portfolioCommentEditedHook = XApiPortfolioCommentEditedHookObserver::create();

        HookLearningPathItemViewed::create()->detach($learningPathItemViewedHook);
        HookLearningPathEnd::create()->detach($learningPathEndHook);
        HookQuizQuestionAnswered::create()->detach($quizQuestionAnsweredHook);
        HookQuizEnd::create()->detach($quizEndHook);
        HookCreateCourse::create()->detach($createCourseHook);
        HookPortfolioItemAdded::create()->detach($portfolioItemAddedHook);
        HookPortfolioItemCommented::create()->detach($portfolioItemCommentedHook);
        HookPortfolioItemHighlighted::create()->detach($portfolioItemHighlightedHook);
        HookPortfolioDownloaded::create()->detach($portfolioDownloaded);
        HookPortfolioItemScored::create()->detach($portfolioItemScoredHook);
        HookPortfolioCommentScored::create()->detach($portfolioCommentedScoredHook);
        HookPortfolioItemEdited::create()->detach($portfolioItemEditedHook);
        HookPortfolioCommentEdited::create()->detach($portfolioCommentEditedHook);

        return 1;
    }

    /**
     * @param string|null $lrsUrl
     * @param string|null $lrsAuthUsername
     * @param string|null $lrsAuthPassword
     *
     * @return \Xabbuh\XApi\Client\Api\StateApiClientInterface
     */
    public function getXApiStateClient($lrsUrl = null, $lrsAuthUsername = null, $lrsAuthPassword = null)
    {
        return $this
            ->createXApiClient($lrsUrl, $lrsAuthUsername, $lrsAuthPassword)
            ->getStateApiClient();
    }

    public function getXApiStatementClient(): StatementsApiClientInterface
    {
        return $this->createXApiClient()->getStatementsApiClient();
    }

    public function getXapiStatementCronClient(): StatementsApiClientInterface
    {
        $lrsUrl = $this->get(self::SETTING_CRON_LRS_URL);
        $lrsUsername = $this->get(self::SETTING_CRON_LRS_AUTH_USERNAME);
        $lrsPassword = $this->get(self::SETTING_CRON_LRS_AUTH_PASSWORD);

        return $this
            ->createXApiClient(
                empty($lrsUrl) ? null : $lrsUrl,
                empty($lrsUsername) ? null : $lrsUsername,
                empty($lrsPassword) ? null : $lrsPassword
            )
            ->getStatementsApiClient();
    }

    /**
     * Perform actions after save the plugin configuration.
     *
     * @return \XApiPlugin
     */
    public function performActionsAfterConfigure()
    {
        $learningPathItemViewedHook = XApiLearningPathItemViewedHookObserver::create();
        $learningPathEndHook = XApiLearningPathEndHookObserver::create();
        $quizQuestionAnsweredHook = XApiQuizQuestionAnsweredHookObserver::create();
        $quizEndHook = XApiQuizEndHookObserver::create();
        $portfolioItemAddedHook = XApiPortfolioItemAddedHookObserver::create();
        $portfolioItemCommentedHook = XApiPortfolioItemCommentedHookObserver::create();
        $portfolioItemViewedHook = XApiPortfolioItemViewedHookObserver::create();
        $portfolioItemHighlightedHook = XApiPortfolioItemHighlightedHookObserver::create();
        $portfolioDownloadedHook = XApiPortfolioDownloadedHookObserver::create();
        $portfolioItemScoredHook = XApiPortfolioItemScoredHookObserver::create();
        $portfolioCommentScoredHook = XApiPortfolioCommentScoredHookObserver::create();
        $portfolioItemEditedHook = XApiPortfolioItemEditedHookObserver::create();
        $portfolioCommentEditedHook = XApiPortfolioCommentEditedHookObserver::create();

        $learningPathItemViewedEvent = HookLearningPathItemViewed::create();
        $learningPathEndEvent = HookLearningPathEnd::create();
        $quizQuestionAnsweredEvent = HookQuizQuestionAnswered::create();
        $quizEndEvent = HookQuizEnd::create();
        $portfolioItemAddedEvent = HookPortfolioItemAdded::create();
        $portfolioItemCommentedEvent = HookPortfolioItemCommented::create();
        $portfolioItemViewedEvent = HookPortfolioItemViewed::create();
        $portfolioItemHighlightedEvent = HookPortfolioItemHighlighted::create();
        $portfolioDownloadedEvent = HookPortfolioDownloaded::create();
        $portfolioItemScoredEvent = HookPortfolioItemScored::create();
        $portfolioCommentScoredEvent = HookPortfolioCommentScored::create();
        $portfolioItemEditedEvent = HookPortfolioItemEdited::create();
        $portfolioCommentEditedEvent = HookPortfolioCommentEdited::create();

        if ('true' === $this->get(self::SETTING_LRS_LP_ITEM_ACTIVE)) {
            $learningPathItemViewedEvent->attach($learningPathItemViewedHook);
        } else {
            $learningPathItemViewedEvent->detach($learningPathItemViewedHook);
        }

        if ('true' === $this->get(self::SETTING_LRS_LP_ACTIVE)) {
            $learningPathEndEvent->attach($learningPathEndHook);
        } else {
            $learningPathEndEvent->detach($learningPathEndHook);
        }

        if ('true' === $this->get(self::SETTING_LRS_QUIZ_ACTIVE)) {
            $quizQuestionAnsweredEvent->attach($quizQuestionAnsweredHook);
        } else {
            $quizQuestionAnsweredEvent->detach($quizQuestionAnsweredHook);
        }

        if ('true' === $this->get(self::SETTING_LRS_QUIZ_QUESTION_ACTIVE)) {
            $quizEndEvent->attach($quizEndHook);
        } else {
            $quizEndEvent->detach($quizEndHook);
        }

        if ('true' === $this->get(self::SETTING_LRS_PORTFOLIO_ACTIVE)) {
            $portfolioItemAddedEvent->attach($portfolioItemAddedHook);
            $portfolioItemCommentedEvent->attach($portfolioItemCommentedHook);
            $portfolioItemViewedEvent->attach($portfolioItemViewedHook);
            $portfolioItemHighlightedEvent->attach($portfolioItemHighlightedHook);
            $portfolioDownloadedEvent->attach($portfolioDownloadedHook);
            $portfolioItemScoredEvent->attach($portfolioItemScoredHook);
            $portfolioCommentScoredEvent->attach($portfolioCommentScoredHook);
            $portfolioItemEditedEvent->attach($portfolioItemEditedHook);
            $portfolioCommentEditedEvent->attach($portfolioCommentEditedHook);
        } else {
            $portfolioItemAddedEvent->detach($portfolioItemAddedHook);
            $portfolioItemCommentedEvent->detach($portfolioItemCommentedHook);
            $portfolioItemViewedEvent->detach($portfolioItemViewedHook);
            $portfolioItemHighlightedEvent->detach($portfolioItemHighlightedHook);
            $portfolioDownloadedEvent->detach($portfolioDownloadedHook);
            $portfolioItemScoredEvent->detach($portfolioItemScoredHook);
            $portfolioCommentScoredEvent->detach($portfolioCommentScoredHook);
            $portfolioItemEditedEvent->detach($portfolioItemEditedHook);
            $portfolioCommentEditedEvent->detach($portfolioCommentEditedHook);
        }

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function installHook()
    {
        $createCourseHook = XApiCreateCourseHookObserver::create();

        HookCreateCourse::create()->attach($createCourseHook);
    }

    /**
     * @param string $variable
     *
     * @return array
     */
    public function getLangMap($variable)
    {
        $platformLanguage = api_get_setting('platformLanguage');
        $platformLanguageIso = api_get_language_isocode($platformLanguage);

        $map = [];
        $map[$platformLanguageIso] = $this->getLangFromFile($variable, $platformLanguage);

        try {
            $interfaceLanguage = api_get_interface_language();
        } catch (Exception $e) {
            return $map;
        }

        if (!empty($interfaceLanguage) && $platformLanguage !== $interfaceLanguage) {
            $interfaceLanguageIso = api_get_language_isocode($interfaceLanguage);

            $map[$interfaceLanguageIso] = $this->getLangFromFile($variable, $interfaceLanguage);
        }

        return $map;
    }

    /**
     * @param string $value
     * @param string $type
     *
     * @return \Xabbuh\XApi\Model\IRI
     */
    public function generateIri($value, $type)
    {
        return IRI::fromString(
            api_get_path(WEB_PATH)."xapi/$type/$value"
        );
    }

    /**
     * @param int $courseId
     */
    public function addCourseToolForTinCan($courseId)
    {
        // The $link param is set to "../plugin" as a hack to link correctly to the plugin URL in course tool.
        // Otherwise, the link en the course tool will link to "/main/" URL.
        $this->createLinkToCourseTool(
            $this->get_lang('ToolTinCan'),
            $courseId,
            'sessions_category.png',
            '../plugin/xapi/start.php',
            0,
            'authoring'
        );
    }

    /**
     * @param string $language
     *
     * @return mixed|string
     */
    public static function extractVerbInLanguage(Xabbuh\XApi\Model\LanguageMap $languageMap, $language)
    {
        $iso = self::findLanguageIso($languageMap->languageTags(), $language);

        $text = current($languageMap);

        if (isset($languageMap[$iso])) {
            $text = trim($languageMap[$iso]);
        } elseif (isset($languageMap['und'])) {
            $text = $languageMap['und'];
        }

        return $text;
    }

    /**
     * @param string $needle
     *
     * @return string
     */
    public static function findLanguageIso(array $haystack, $needle)
    {
        if (in_array($needle, $haystack)) {
            return $needle;
        }

        foreach ($haystack as $language) {
            if (strpos($language, $needle) === 0) {
                return $language;
            }
        }

        return $haystack[0];
    }

    public function generateLaunchUrl(
        $type,
        $launchUrl,
        $activityId,
        Agent $actor,
        $attemptId,
        $customLrsUrl = null,
        $customLrsUsername = null,
        $customLrsPassword = null,
        $viewSessionId = null
    ) {
        $lrsUrl = $customLrsUrl ?: $this->get(self::SETTING_LRS_URL);
        $lrsAuthUsername = $customLrsUsername ?: $this->get(self::SETTING_LRS_AUTH_USERNAME);
        $lrsAuthPassword = $customLrsPassword ?: $this->get(self::SETTING_LRS_AUTH_PASSWORD);

        $queryData = [
            'endpoint' => trim($lrsUrl, "/ \t\n\r\0\x0B"),
            'actor' => Serializer::createSerializer()->serialize($actor, 'json'),
            'registration' => $attemptId,
        ];

        if ('tincan' === $type) {
            $queryData['auth'] = 'Basic '.base64_encode(trim($lrsAuthUsername).':'.trim($lrsAuthPassword));
            $queryData['activity_id'] = $activityId;
        } elseif ('cmi5' === $type) {
            $queryData['fetch'] = api_get_path(WEB_PLUGIN_PATH).'xapi/cmi5/token.php?session='.$viewSessionId;
            $queryData['activityId'] = $activityId;
        }

        return $launchUrl.'?'.http_build_query($queryData, null, '&', PHP_QUERY_RFC3986);
    }

    /**
     * @return \Doctrine\ORM\EntityManager|null
     */
    public static function getEntityManager()
    {
        $em = Database::getManager();

        $prefixes = [
            __DIR__.'/../php-xapi/repository-doctrine-orm/metadata' => 'XApi\Repository\Doctrine\Mapping',
        ];

        $driver = new SimplifiedXmlDriver($prefixes);
        $driver->setGlobalBasename('global');

        $config = Database::getDoctrineConfig(api_get_configuration_value('root_sys'));
        $config->setMetadataDriverImpl($driver);

        try {
            return EntityManager::create($em->getConnection()->getParams(), $config);
        } catch (ORMException $e) {
            api_not_allowed(true, $e->getMessage());
        }

        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function getAdminUrl()
    {
        $webPath = api_get_path(WEB_PLUGIN_PATH).$this->get_name();

        return "$webPath/admin.php";
    }

    public function getLpResourceBlock(int $lpId)
    {
        $cidReq = api_get_cidreq(true, true, 'lp');
        $webPath = api_get_path(WEB_PLUGIN_PATH).'xapi/';
        $course = api_get_course_entity();
        $session = api_get_session_entity();

        $tools = Database::getManager()
            ->getRepository(XApiToolLaunch::class)
            ->findByCourseAndSession($course, $session);

        $importIcon = Display::return_icon('import_scorm.png');
        $moveIcon = Display::url(
            Display::return_icon('move_everywhere.png', get_lang('Move'), [], ICON_SIZE_TINY),
            '#',
            ['class' => 'moved']
        );

        $return = '<ul class="lp_resource"><li class="lp_resource_element">'
            .$importIcon
            .Display::url(
                get_lang('Import'),
                $webPath."tool_import.php?$cidReq&".http_build_query(['lp_id' => $lpId])
            )
            .'</li>';

        foreach ($tools as $tool) {
            $toolAnchor = Display::url(
                Security::remove_XSS($tool->getTitle()),
                api_get_self()."?$cidReq&"
                    .http_build_query(
                        ['action' => 'add_item', 'type' => TOOL_XAPI, 'file' => $tool->getId(), 'lp_id' => $lpId]
                    ),
                ['class' => 'moved']
            );

            $return .= Display::tag(
                'li',
                $moveIcon.$importIcon.$toolAnchor,
                [
                    'class' => 'lp_resource_element',
                    'data_id' => $tool->getId(),
                    'data_type' => TOOL_XAPI,
                    'title' => $tool->getTitle(),
                ]
            );
        }

        $return .= '</ul>';

        return $return;
    }

    /**
     * @throws \Exception
     */
    private function installInitialConfig()
    {
        $uuidNamespace = Uuid::v1()->toRfc4122();

        $pluginName = $this->get_name();
        $urlId = api_get_current_access_url_id();

        api_add_setting(
            $uuidNamespace,
            $pluginName.'_'.self::SETTING_UUID_NAMESPACE,
            $pluginName,
            'setting',
            'Plugins',
            $pluginName,
            '',
            '',
            '',
            $urlId,
            1
        );

        api_add_setting(
            api_get_path(WEB_PATH).'plugin/xapi/lrs.php',
            $pluginName.'_'.self::SETTING_LRS_URL,
            $pluginName,
            'setting',
            'Plugins',
            $pluginName,
            '',
            '',
            '',
            $urlId,
            1
        );
    }

    /**
     * @param string|null $lrsUrl
     * @param string|null $lrsAuthUsername
     * @param string|null $lrsAuthPassword
     *
     * @return \Xabbuh\XApi\Client\XApiClientInterface
     */
    private function createXApiClient($lrsUrl = null, $lrsAuthUsername = null, $lrsAuthPassword = null)
    {
        $baseUrl = $lrsUrl ?: $this->get(self::SETTING_LRS_URL);
        $lrsAuthUsername = $lrsAuthUsername ?: $this->get(self::SETTING_LRS_AUTH_USERNAME);
        $lrsAuthPassword = $lrsAuthPassword ?: $this->get(self::SETTING_LRS_AUTH_PASSWORD);

        $clientBuilder = new XApiClientBuilder();
        $clientBuilder
            ->setHttpClient(Client::createWithConfig([RequestOptions::VERIFY => false]))
            ->setRequestFactory(new GuzzleMessageFactory())
            ->setBaseUrl(trim($baseUrl, "/ \t\n\r\0\x0B"))
            ->setAuth(trim($lrsAuthUsername), trim($lrsAuthPassword));

        return $clientBuilder->build();
    }

    private function addCourseTools()
    {
        $courses = Database::getManager()
            ->createQuery('SELECT c.id FROM ChamiloCoreBundle:Course c')
            ->getResult();

        foreach ($courses as $course) {
            $this->addCourseToolForTinCan($course['id']);
        }
    }

    private function deleteCourseTools()
    {
        Database::getManager()
            ->createQuery('DELETE FROM ChamiloCourseBundle:CTool t WHERE t.category = :category AND t.link LIKE :link')
            ->execute(['category' => 'authoring', 'link' => '../plugin/xapi/start.php%']);
    }
}