WIP Integrating LTI - refs BT#13469

pull/2818/head
Angel Fernando Quiroz Campos 6 years ago
parent b08507cf4e
commit 6d643758e7
  1. 1
      config/bundles.php
  2. 3
      config/routes/chamilo.yaml
  3. 25
      main/course_info/infocours.php
  4. 66
      src/CoreBundle/Migrations/Schema/V200/Version20181126174500.php
  5. 161
      src/IntegrationBundle/Controller/AdminController.php
  6. 851
      src/IntegrationBundle/Controller/CourseController.php
  7. 135
      src/IntegrationBundle/Controller/ServiceController.php
  8. 504
      src/IntegrationBundle/Entity/ExternalTool.php
  9. 176
      src/IntegrationBundle/Form/ExternalToolType.php
  10. 14
      src/IntegrationBundle/Resources/config/routing.yml
  11. 62
      src/ThemeBundle/Resources/views/Lti/admin.html.twig
  12. 14
      src/ThemeBundle/Resources/views/Lti/admin_form.html.twig
  13. 70
      src/ThemeBundle/Resources/views/Lti/course_configure.twig
  14. 20
      src/ThemeBundle/Resources/views/Lti/iframe.html.twig
  15. 7
      src/ThemeBundle/Resources/views/Lti/item_return.html.twig
  16. 18
      src/ThemeBundle/Resources/views/Lti/launch.html.twig

@ -55,6 +55,7 @@ return [
Chamilo\SettingsBundle\ChamiloSettingsBundle::class => ['all' => true],
Chamilo\TimelineBundle\ChamiloTimelineBundle::class => ['all' => true],
Chamilo\ApiBundle\ChamiloApiBundle::class => ['all' => true],
Chamilo\IntegrationBundle\ChamiloIntegrationBundle::class => ['all' => true],
winzou\Bundle\StateMachineBundle\winzouStateMachineBundle::class => ['all' => true],
Bazinga\Bundle\HateoasBundle\BazingaHateoasBundle::class => ['all' => true],
Sylius\Bundle\ResourceBundle\SyliusResourceBundle::class => ['all' => true],

@ -28,3 +28,6 @@ chamilo_faq:
chamilo_graphql:
resource: "@ChamiloApiBundle/GraphQL/Resources/config/routing.yaml"
chamilo_lti:
resource: "@ChamiloIntegrationBundle/Resources/config/routing.yml"

@ -15,6 +15,9 @@
*
* @package chamilo.course_info
*/
use Chamilo\CoreBundle\Framework\Container;
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_COURSE_SETTING;
$this_section = SECTION_COURSES;
@ -32,6 +35,9 @@ if (!$isAllowToEdit) {
api_not_allowed(true);
}
$router = Container::getRouter();
$translator = Container::getTranslator();
$show_delete_watermark_text_message = false;
if (api_get_setting('pdf_export_watermark_by_course') == 'true') {
if (isset($_GET['delete_watermark'])) {
@ -885,6 +891,25 @@ $form->addPanelOption(
'accordionSettings'
);
$button = Display::toolbarButton(
$translator->trans('Configure external tool'),
$router->generate('chamilo_lti_configure', ['code' => $course_code]),
'cog',
'primary'
);
$html = [
$form->createElement('html', '<p>'.get_lang('LTI intro tool').'</p>'.$button),
];
$form->addPanelOption(
'lti_tool',
$translator->trans('External tools'),
$html,
'plugin.png',
false,
'accordionSettings'
);
// Plugin course settings
$appPlugin = new AppPlugin();
$appPlugin->add_course_settings_form($form);

@ -0,0 +1,66 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Doctrine\DBAL\Schema\Schema;
/**
* Class Version20181126174500
*
* @package Chamilo\CoreBundle\Migrations\Schema\V200
*/
class Version20181126174500 extends AbstractMigrationChamilo
{
/**
* @param \Doctrine\DBAL\Schema\Schema $schema
*/
public function up(Schema $schema)
{
if ($schema->hasTable('plugin_ims_lti_tool')) {
$schema->renameTable('plugin_ims_lti_tool', 'lti_external_tool');
return;
}
$this->addSql(
'CREATE TABLE lti_external_tool (
id INT AUTO_INCREMENT NOT NULL,
c_id INT DEFAULT NULL,
gradebook_eval_id INT DEFAULT NULL,
parent_id INT DEFAULT NULL,
name VARCHAR(255) NOT NULL,
description LONGTEXT DEFAULT NULL,
launch_url VARCHAR(255) NOT NULL,
consumer_key VARCHAR(255) DEFAULT NULL,
shared_secret VARCHAR(255) DEFAULT NULL,
custom_params LONGTEXT DEFAULT NULL,
active_deep_linking TINYINT(1) DEFAULT \'0\' NOT NULL,
privacy LONGTEXT DEFAULT NULL,
INDEX IDX_DB0E04E491D79BD3 (c_id),
INDEX IDX_DB0E04E482F80D8B (gradebook_eval_id),
INDEX IDX_DB0E04E4727ACA70 (parent_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB ROW_FORMAT = DYNAMIC'
);
$this->addSql(
'ALTER TABLE lti_external_tool ADD CONSTRAINT FK_DB0E04E491D79BD3 FOREIGN KEY (c_id) REFERENCES course (id)'
);
$this->addSql(
'ALTER TABLE lti_external_tool ADD CONSTRAINT FK_DB0E04E482F80D8B FOREIGN KEY (gradebook_eval_id)
REFERENCES gradebook_evaluation (id) ON DELETE SET NULL;'
);
$this->addSql(
'ALTER TABLE lti_external_tool ADD CONSTRAINT FK_DB0E04E4727ACA70 FOREIGN KEY (parent_id)
REFERENCES lti_external_tool (id);'
);
}
/**
* @param \Doctrine\DBAL\Schema\Schema $schema
*/
public function down(Schema $schema)
{
}
}

@ -0,0 +1,161 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\IntegrationBundle\Controller;
use Chamilo\CoreBundle\Controller\BaseController;
use Chamilo\IntegrationBundle\Entity\ExternalTool;
use Chamilo\IntegrationBundle\Form\ExternalToolType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* Class AdminController.
*
* @package Chamilo\IntegrationBundle\Controller
*/
class AdminController extends BaseController
{
/**
* @Route("/", name="chamilo_lti_admin")
*
* @Security("has_role('ROLE_ADMIN')")
*
* @return Response
*/
public function adminAction(): Response
{
$repo = $this->getDoctrine()->getRepository('ChamiloIntegrationBundle:ExternalTool');
$tools = $repo->findAll();
return $this->render('@ChamiloTheme/Lti/admin.html.twig', ['tools' => $tools]);
}
/**
* @Route("/add", name="chamilo_lti_admin_add")
*
* @Security("has_role('ROLE_ADMIN')")
*
* @param Request $request
*
* @return Response
*/
public function adminAddAction(Request $request): Response
{
$form = $this->createForm(ExternalToolType::class, new ExternalTool());
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var ExternalTool $tool */
$tool = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($tool);
$em->flush();
$this->addFlash('success', $this->trans('External tool added'));
return $this->redirectToRoute('chamilo_lti_admin');
}
$breadcrumb = $this->get('chamilo_core.block.breadcrumb');
$breadcrumb->addChild(
$this->trans('Administration'),
['route' => 'administration']
);
$breadcrumb->addChild(
$this->trans('External tools'),
['route' => 'chamilo_lti_admin']
);
$breadcrumb->addChild('Add external tool');
return $this->render(
'@ChamiloTheme/Lti/admin_form.html.twig',
['form' => $form->createView()]
);
}
/**
* @Route("/edit/{toolId}", name="chamilo_lti_admin_edit", requirements={"toolId"="\d+"})
*
* @Security("has_role('ROLE_ADMIN')")
*
* @param int $toolId
* @param Request $request
*
* @return Response
*/
public function adminEditAction($toolId, Request $request)
{
$em = $this->getDoctrine()->getManager();
/** @var ExternalTool $tool */
$tool = $em->find('ChamiloIntegrationBundle:ExternalTool', $toolId);
if (empty($tool)) {
throw $this->createNotFoundException();
}
$form = $this->createForm(ExternalToolType::class, $tool);
$form->get('shareName')->setData($tool->isSharingName());
$form->get('shareEmail')->setData($tool->isSharingEmail());
$form->get('sharePicture')->setData($tool->isSharingPicture());
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var ExternalTool $tool */
$tool = $form->getData();
$em->persist($tool);
$em->flush();
$this->addFlash('success', $this->trans('External tool edited'));
return $this->redirectToRoute('chamilo_lti_admin');
}
$breadcrumb = $this->get('chamilo_core.block.breadcrumb');
$breadcrumb->addChild(
$this->trans('Administration'),
['route' => 'administration']
);
$breadcrumb->addChild(
$this->trans('External tools'),
['route' => 'chamilo_lti_admin']
);
$breadcrumb->addChild('Edit external tool');
return $this->render(
'@ChamiloTheme/Lti/admin_form.html.twig',
['form' => $form->createView()]
);
}
/**
* @Route("/delete/{toolId}", name="chamilo_lti_admin_delete", requirements={"toolId"="\d+"})
*
* @Security("has_role('ROLE_ADMIN')")
*
* @param int $toolId
*
* @return Response
*/
public function adminDeleteAction($toolId)
{
$em = $this->getDoctrine()->getManager();
/** @var ExternalTool $tool */
$tool = $em->find('ChamiloIntegrationBundle:ExternalTool', $toolId);
if (empty($tool)) {
throw $this->createNotFoundException();
}
$em->remove($tool);
$em->flush();
$this->addFlash('success', $this->trans('External tool deleted'));
return $this->redirectToRoute('chamilo_lti_admin');
}
}

@ -0,0 +1,851 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\IntegrationBundle\Controller;
use Chamilo\CoreBundle\Controller\BaseController;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CourseBundle\Entity\CTool;
use Chamilo\IntegrationBundle\Component\ServiceRequestFactory;
use Chamilo\IntegrationBundle\Entity\ExternalTool;
use Chamilo\IntegrationBundle\Form\ExternalToolType;
use Chamilo\UserBundle\Entity\User;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* Class CourseController.
*
* @package Chamilo\IntegrationBundle\Controller
*/
class CourseController extends BaseController
{
/**
* @Route("/edit/{id}", name="chamilo_lti_edit", requirements={"id"="\d+"})
*
* @param string $id
* @param Request $request
*
* @return Response
*/
public function editAction($id, Request $request): Response
{
$em = $this->getDoctrine()->getManager();
/** @var ExternalTool $tool */
$tool = $em->find('ChamiloIntegrationBundle:ExternalTool', $id);
if (empty($tool)) {
throw $this->createNotFoundException('External tool not found');
}
$course = $this->getCourse();
$form = $this->createForm(ExternalToolType::class, $tool);
$form->get('shareName')->setData($tool->isSharingName());
$form->get('shareEmail')->setData($tool->isSharingEmail());
$form->get('sharePicture')->setData($tool->isSharingPicture());
$form->handleRequest($request);
if (!$form->isSubmitted() || !$form->isValid()) {
$this->setConfigureBreadcrumb($course);
return $this->render(
'@ChamiloTheme/Lti/course_configure.twig',
[
'title' => $this->trans('Edit external tool'),
'added_tools' => [],
'global_tools' => [],
'form' => $form->createView(),
'course' => $course,
]
);
}
/** @var ExternalTool $tool */
$tool = $form->getData();
$em->persist($tool);
if (!$tool->isActiveDeepLinking()) {
$courseTool = $em->getRepository('ChamiloCourseBundle:CTool')
->findOneBy(
[
'course' => $course,
'link' => $this->generateUrl(
'chamilo_lti_show',
['code' => $course->getCode(), 'id' => $tool->getId()]
),
]
);
if (empty($courseTool)) {
throw $this->createNotFoundException('Course tool not found.');
}
$courseTool->setName($tool->getName());
$em->persist($courseTool);
}
$em->flush();
$this->addFlash('success', $this->trans('External tool edited'));
return $this->redirectToRoute(
'chamilo_lti_edit',
['id' => $tool->getId(), 'code' => $course->getCode()]
);
}
/**
* @param Course $course
*/
private function setConfigureBreadcrumb(Course $course)
{
$breadcrumb = $this->get('chamilo_core.block.breadcrumb');
$breadcrumb->addChild(
$course->getTitle(),
[
'uri' => $this->generateUrl(
'chamilo_course_home_home_index',
['course' => $course->getCode()]
),
]
);
$breadcrumb->addChild(
$this->trans('Configure external tool')
);
}
/**
* @Route("/launch/{id}", name="chamilo_lti_launch", requirements={"id"="\d+"})
*
* @param string $id
*
* @return Response
*/
public function launchAction($id): Response
{
$em = $this->getDoctrine()->getManager();
/** @var ExternalTool|null $tool */
$tool = $em->find('ChamiloIntegrationBundle:ExternalTool', $id);
if (empty($tool)) {
throw $this->createNotFoundException();
}
$settingsManager = $this->get('chamilo.settings.manager');
/** @var User $user */
$user = $this->getUser();
$course = $this->getCourse();
$session = $this->getSession();
if (empty($tool->getCourse()) || $tool->getCourse()->getId() !== $course->getId()) {
throw $this->createAccessDeniedException('');
}
$institutionDomain = $this->getInstitutionDomain();
$toolUserId = $this->getToolUserId($user->getId());
$params = [];
$params['lti_version'] = 'LTI-1p0';
if ($tool->isActiveDeepLinking()) {
$params['lti_message_type'] = 'ContentItemSelectionRequest';
$params['content_item_return_url'] = $this->generateUrl(
'chamilo_lti_return_item',
['code' => $course->getCode()],
UrlGeneratorInterface::ABSOLUTE_URL
);
$params['accept_media_types'] = '*/*';
$params['accept_presentation_document_targets'] = 'iframe';
$params['title'] = $tool->getName();
$params['text'] = $tool->getDescription();
$params['data'] = 'tool:'.$tool->getId();
} else {
$params['lti_message_type'] = 'basic-lti-launch-request';
$params['resource_link_id'] = $tool->getId();
$params['resource_link_title'] = $tool->getName();
$params['resource_link_description'] = $tool->getDescription();
$toolEval = $tool->getGradebookEval();
if (!empty($toolEval)) {
$params['lis_result_sourcedid'] = json_encode(
['e' => $toolEval->getId(), 'u' => $user->getId(), 'l' => uniqid(), 'lt' => time()]
);
$params['lis_outcome_service_url'] = api_get_path(WEB_PATH).'lti/os';
$params['lis_outcome_service_url'] = $this->generateUrl(
'chamilo_lti_os',
[],
UrlGeneratorInterface::ABSOLUTE_URL
);
$params['lis_person_sourcedid'] = "$institutionDomain:$toolUserId";
$params['lis_course_section_sourcedid'] = "$institutionDomain:".$course->getId();
if ($session) {
$params['lis_course_section_sourcedid'] .= ':'.$session->getId();
}
}
}
$params['user_id'] = $toolUserId;
if ($tool->isSharingPicture()) {
$params['user_image'] = \UserManager::getUserPicture($user->getId());
}
$params['roles'] = $this->getUserRoles($user);
if ($tool->isSharingName()) {
$params['lis_person_name_given'] = $user->getFirstname();
$params['lis_person_name_family'] = $user->getLastname();
$params['lis_person_name_full'] = $user->getFirstname().' '.$user->getLastname();
}
if ($tool->isSharingEmail()) {
$params['lis_person_contact_email_primary'] = $user->getEmail();
}
if ($user->hasRole('ROLE_RRHH')) {
$scopeMentor = $this->getRoleScopeMentor($user);
if (!empty($scopeMentor)) {
$params['role_scope_mentor'] = $scopeMentor;
}
}
$params['context_id'] = $course->getId();
$params['context_type'] = 'CourseSection';
$params['context_label'] = $course->getCode();
$params['context_title'] = $course->getTitle();
$params['launch_presentation_locale'] = 'en';
$params['launch_presentation_document_target'] = 'iframe';
$params['tool_consumer_info_product_family_code'] = 'Chamilo LMS';
$params['tool_consumer_info_version'] = '2.0';
$params['tool_consumer_instance_guid'] = $institutionDomain;
$params['tool_consumer_instance_name'] = $settingsManager->getSetting('platform.site_name');
$params['tool_consumer_instance_url'] = $this->generateUrl(
'home',
[],
UrlGeneratorInterface::ABSOLUTE_URL
);
$params['tool_consumer_instance_contact_email'] = $settingsManager->getSetting('admin.administrator_email');
$params['oauth_callback'] = 'about:blank';
$customParams = $tool->parseCustomParams();
$this->trimParams($customParams);
$this->variableSubstitution($params, $customParams, $user, $course, $session);
$params += $customParams;
$this->trimParams($params);
if (!empty($tool->getConsumerKey()) && !empty($tool->getSharedSecret())) {
$consumer = new \OAuthConsumer(
$tool->getConsumerKey(),
$tool->getSharedSecret(),
null
);
$hmacMethod = new \OAuthSignatureMethod_HMAC_SHA1();
$request = \OAuthRequest::from_consumer_and_token(
$consumer,
'',
'POST',
$tool->getLaunchUrl(),
$params
);
$request->sign_request($hmacMethod, $consumer, '');
$params = $request->get_parameters();
}
$this->removeQueryParamsFromLaunchUrl($tool, $params);
return $this->render(
'@ChamiloTheme/Lti/launch.html.twig',
[
'params' => $params,
'launch_url' => $tool->getLaunchUrl(),
]
);
}
/**
* @return string
*/
private function getInstitutionDomain()
{
$institutionUrl = $this->get('chamilo.settings.manager')->getSetting('platform.institution_url');
return str_replace(['https://', 'http://'], '', $institutionUrl);
}
/**
* @param int $userId
*
* @return string
*/
private function getToolUserId($userId)
{
$manager = $this->get('chamilo.settings.manager');
$siteName = $manager->getSetting('platform.site_name');
$institution = $manager->getSetting('platform.institution');
$userString = "$siteName - $institution - $userId";
return \URLify::filter($userString, 255, '', true, true, false, false, true);
}
/**
* @param User $user
*
* @return string
*/
private function getUserRoles(User $user)
{
if ($user->hasRole('ROLE_RRHH')) {
return 'urn:lti:role:ims/lis/Mentor';
}
//if ($user->hasRole('ROLE_INVITEE')) {
// return 'Learner,urn:lti:role:ims/lis/Learner/GuestLearner';
//}
if ($user->hasRole('ROLE_CURRENT_COURSE_STUDENT') || $user->hasRole('ROLE_CURRENT_SESSION_COURSE_STUDENT')) {
return 'Learner';
}
$roles = ['Instructor'];
if ($user->hasRole('ROLE_ADMIN')) {
$roles[] = 'urn:lti:role:ims/lis/Administrator';
}
return implode(',', $roles);
}
/**
* @param User $currentUser
*
* @return string
*/
private function getRoleScopeMentor(User $currentUser)
{
if (DRH !== $currentUser->getStatus()) {
return '';
}
$followedUsers = \UserManager::get_users_followed_by_drh($currentUser->getId());
$scope = [];
foreach ($followedUsers as $userInfo) {
$scope[] = $this->getToolUserId($userInfo['user_id']);
}
return implode(',', $scope);
}
/**
* @param array $params
*/
private function trimParams(array &$params)
{
foreach ($params as $key => $value) {
$newValue = preg_replace('/\s+/', ' ', $value);
$params[$key] = trim($newValue);
}
}
/**
* @param array $params
* @param array $customParams
* @param User $user
* @param Course $course
* @param Session|null $session
*/
private function variableSubstitution(
array $params,
array &$customParams,
User $user,
Course $course,
Session $session = null
) {
$replaceable = self::getReplaceableVariables($user, $course, $session);
$variables = array_keys($replaceable);
foreach ($customParams as $customKey => $customValue) {
if (!in_array($customValue, $variables)) {
continue;
}
$val = $replaceable[$customValue];
if (is_array($val)) {
$val = current($val);
if (array_key_exists($val, $params)) {
$customParams[$customKey] = $params[$val];
continue;
} else {
$val = false;
}
}
if (false === $val) {
$customParams[$customKey] = $customValue;
continue;
}
$customParams[$customKey] = $replaceable[$customValue];
}
}
/**
* @param User $user
* @param Course $course
* @param Session|null $session
*
* @return array
*/
private static function getReplaceableVariables(User $user, Course $course, Session $session = null)
{
return [
'$User.id' => $user->getId(),
'$User.image' => ['user_image'],
'$User.username' => $user->getUsername(),
'$Person.sourcedId' => false,
'$Person.name.full' => $user->getFullname(),
'$Person.name.family' => $user->getLastname(),
'$Person.name.given' => $user->getFirstname(),
'$Person.name.middle' => false,
'$Person.name.prefix' => false,
'$Person.name.suffix' => false,
'$Person.address.street1' => $user->getAddress(),
'$Person.address.street2' => false,
'$Person.address.street3' => false,
'$Person.address.street4' => false,
'$Person.address.locality' => false,
'$Person.address.statepr' => false,
'$Person.address.country' => false,
'$Person.address.postcode' => false,
'$Person.address.timezone' => false, //$user->getTimezone(),
'$Person.phone.mobile' => false,
'$Person.phone.primary' => $user->getPhone(),
'$Person.phone.home' => false,
'$Person.phone.work' => false,
'$Person.email.primary' => $user->getEmail(),
'$Person.email.personal' => false,
'$Person.webaddress' => false, //$user->getWebsite(),
'$Person.sms' => false,
'$CourseTemplate.sourcedId' => false,
'$CourseTemplate.label' => false,
'$CourseTemplate.title' => false,
'$CourseTemplate.shortDescription' => false,
'$CourseTemplate.longDescription' => false,
'$CourseTemplate.courseNumber' => false,
'$CourseTemplate.credits' => false,
'$CourseOffering.sourcedId' => false,
'$CourseOffering.label' => false,
'$CourseOffering.title' => false,
'$CourseOffering.shortDescription' => false,
'$CourseOffering.longDescription' => false,
'$CourseOffering.courseNumber' => false,
'$CourseOffering.credits' => false,
'$CourseOffering.academicSession' => false,
'$CourseSection.sourcedId' => ['lis_course_section_sourcedid'],
'$CourseSection.label' => $course->getCode(),
'$CourseSection.title' => $course->getTitle(),
'$CourseSection.shortDescription' => false,
'$CourseSection.longDescription' => $session && $session->getShowDescription()
? $session->getDescription()
: false,
'$CourseSection.courseNumber' => false,
'$CourseSection.credits' => false,
'$CourseSection.maxNumberofStudents' => false,
'$CourseSection.numberofStudents' => false,
'$CourseSection.dept' => false,
'$CourseSection.timeFrame.begin' => $session && $session->getDisplayStartDate()
? $session->getDisplayStartDate()->format(\DateTime::ATOM)
: false,
'$CourseSection.timeFrame.end' => $session && $session->getDisplayEndDate()
? $session->getDisplayEndDate()->format(\DateTime::ATOM)
: false,
'$CourseSection.enrollControl.accept' => false,
'$CourseSection.enrollControl.allowed' => false,
'$CourseSection.dataSource' => false,
'$CourseSection.sourceSectionId' => false,
'$Group.sourcedId' => false,
'$Group.grouptype.scheme' => false,
'$Group.grouptype.typevalue' => false,
'$Group.grouptype.level' => false,
'$Group.email' => false,
'$Group.url' => false,
'$Group.timeFrame.begin' => false,
'$Group.timeFrame.end' => false,
'$Group.enrollControl.accept' => false,
'$Group.enrollControl.allowed' => false,
'$Group.shortDescription' => false,
'$Group.longDescription' => false,
'$Group.parentId' => false,
'$Membership.sourcedId' => false,
'$Membership.collectionSourcedId' => false,
'$Membership.personSourcedId' => false,
'$Membership.status' => false,
'$Membership.role' => ['roles'],
'$Membership.createdTimestamp' => false,
'$Membership.dataSource' => false,
'$LineItem.sourcedId' => false,
'$LineItem.type' => false,
'$LineItem.type.displayName' => false,
'$LineItem.resultValue.max' => false,
'$LineItem.resultValue.list' => false,
'$LineItem.dataSource' => false,
'$Result.sourcedGUID' => ['lis_result_sourcedid'],
'$Result.sourcedId' => ['lis_result_sourcedid'],
'$Result.createdTimestamp' => false,
'$Result.status' => false,
'$Result.resultScore' => false,
'$Result.dataSource' => false,
'$ResourceLink.title' => ['resource_link_title'],
'$ResourceLink.description' => ['resource_link_description'],
];
}
/**
* @param ExternalTool $tool
* @param array $params
*
* @return array
*/
private function removeQueryParamsFromLaunchUrl(ExternalTool $tool, array &$params)
{
$urlQuery = parse_url($tool->getLaunchUrl(), PHP_URL_QUERY);
if (empty($urlQuery)) {
return $params;
}
$queryParams = [];
parse_str($urlQuery, $queryParams);
$queryKeys = array_keys($queryParams);
foreach ($queryKeys as $key) {
if (isset($params[$key])) {
unset($params[$key]);
}
}
}
/**
* @Route("/item_return", name="chamilo_lti_return_item")
*
* @param Request $request
*
* @return Response
*/
public function returnItemAction(Request $request): Response
{
$contentItems = $request->get('content_items');
$data = $request->get('data');
if (empty($contentItems) || empty($data)) {
throw $this->createAccessDeniedException();
}
$em = $this->getDoctrine()->getManager();
/** @var ExternalTool $tool */
$tool = $em->find('ChamiloIntegrationBundle:ExternalTool', str_replace('tool:', '', $data));
if (empty($tool)) {
throw $this->createNotFoundException('External tool not found');
}
$course = $this->getCourse();
$url = $this->generateUrl(
'chamilo_lti_return_item',
['code' => $course->getCode()],
UrlGeneratorInterface::ABSOLUTE_URL
);
$signatureIsValid = $this->compareRequestSignature(
$url,
$request->get('oauth_consumer_key'),
$request->get('oauth_signature'),
$tool
);
if (!$signatureIsValid) {
throw $this->createAccessDeniedException();
}
$contentItems = json_decode($contentItems, true)['@graph'];
$supportedItemTypes = ['LtiLinkItem'];
foreach ($contentItems as $contentItem) {
if (!in_array($contentItem['@type'], $supportedItemTypes)) {
continue;
}
if ('LtiLinkItem' === $contentItem['@type']) {
$newTool = $this->createLtiLink($contentItem, $tool);
$this->addFlash(
'success',
sprintf(
$this->trans('External tool added: %s'),
$newTool->getName()
)
);
}
}
return $this->render(
'@ChamiloTheme/Lti/item_return.html.twig',
['course' => $course]
);
}
/**
* @param string $url
* @param string $originConsumerKey
* @param string $originSignature
* @param ExternalTool $tool
*
* @return bool
*/
private function compareRequestSignature(
$url,
$originConsumerKey,
$originSignature,
ExternalTool $tool
)
{
$consumer = new \OAuthConsumer(
$originConsumerKey,
$tool->getSharedSecret()
);
$hmacMethod = new \OAuthSignatureMethod_HMAC_SHA1();
$oAuthRequest = \OAuthRequest::from_request('POST', $url);
$oAuthRequest->sign_request($hmacMethod, $consumer, '');
$signature = $oAuthRequest->get_parameter('oauth_signature');
return $signature !== $originSignature;
}
/**
* @param array $contentItem
* @param ExternalTool $baseTool
*
* @return ExternalTool
*/
private function createLtiLink(array &$contentItem, ExternalTool $baseTool)
{
$newTool = clone $baseTool;
$newTool->setParent($baseTool);
$newTool->setActiveDeepLinking(false);
if (!empty($contentItem['title'])) {
$newTool->setName($contentItem['title']);
}
if (!empty($contentItem['text'])) {
$newTool->setDescription($contentItem['text']);
}
if (!empty($contentItem['url'])) {
$newTool->setLaunchUrl($contentItem['url']);
}
if (!empty($contentItem['custom'])) {
$newTool->setCustomParams(
$newTool->encodeCustomParams($contentItem['custom'])
);
}
$em = $this->getDoctrine()->getManager();
$em->persist($newTool);
$em->flush();
$course = $newTool->getCourse();
$courseTool = new CTool();
$courseTool
->setCourse($course)
->setImage('plugin.png')
->setName($newTool->getName())
->setVisibility(true)
->setTarget('_self')
->setCategory('interaction')
->setLink(
$this->generateUrl(
'chamilo_lti_show',
['code' => $course->getCode(), 'id' => $newTool->getId()]
)
);
$em->persist($courseTool);
$em->flush();
return $newTool;
}
/**
* @Route("/{id}", name="chamilo_lti_show", requirements={"id"="\d+"})
*
* @param string $id
*
* @return Response
*/
public function showAction($id): Response
{
$course = $this->getCourse();
$em = $this->getDoctrine()->getManager();
/** @var ExternalTool|null $externalTool */
$externalTool = $em->find('ChamiloIntegrationBundle:ExternalTool', $id);
if (empty($externalTool)) {
throw $this->createNotFoundException();
}
if (empty($externalTool->getCourse()) || $externalTool->getCourse()->getId() !== $course->getId()) {
throw $this->createAccessDeniedException('');
}
$breadcrumb = $this->get('chamilo_core.block.breadcrumb');
$breadcrumb->addChild(
$course->getTitle(),
[
'uri' => $this->generateUrl(
'chamilo_course_home_home_index',
['course' => $course->getCode()]
),
]
);
$breadcrumb->addChild(
$this->trans($externalTool->getName())
);
return $this->render(
'ChamiloThemeBundle:Lti:iframe.html.twig',
['tool' => $externalTool, 'course' => $course]
);
}
/**
* @Route("/", name="chamilo_lti_configure")
* @Route("/add/{id}", name="chamilo_lti_configure_global", requirements={"id"="\d+"})
*
* @Security("has_role('ROLE_TEACHER')")
*
* @param string $id
* @param Request $request
*
* @return Response
*/
public function courseConfigureAction($id = '', Request $request): Response
{
$em = $this->getDoctrine()->getManager();
$repo = $em->getRepository('ChamiloIntegrationBundle:ExternalTool');
$tool = new ExternalTool();
$parentTool = null;
if (!empty($id)) {
$parentTool = $repo->findOneBy(['id' => $id, 'course' => null]);
if (empty($parentTool)) {
throw $this->createNotFoundException('External tool not found');
}
$tool = clone $parentTool;
$tool->setParent($parentTool);
}
$course = $this->getCourse();
$form = $this->createForm(ExternalToolType::class, $tool);
$form->get('shareName')->setData($tool->isSharingName());
$form->get('shareEmail')->setData($tool->isSharingEmail());
$form->get('sharePicture')->setData($tool->isSharingPicture());
$form->handleRequest($request);
if (!$form->isSubmitted() || !$form->isValid()) {
$this->setConfigureBreadcrumb($course);
return $this->render(
'@ChamiloTheme/Lti/course_configure.twig',
[
'title' => $this->trans('Add external tool'),
'added_tools' => $repo->findBy(['course' => $course]),
'global_tools' => $repo->findBy(['parent' => null, 'course' => null]),
'form' => $form->createView(),
'course' => $course,
]
);
}
/** @var ExternalTool $tool */
$tool = $form->getData();
$tool->setCourse($course);
$em->persist($tool);
$em->flush();
$this->addFlash('success', $this->trans('External tool added'));
if (!$tool->isActiveDeepLinking()) {
$courseTool = new CTool();
$courseTool
->setCourse($course)
->setImage('plugin.png')
->setName($tool->getName())
->setVisibility(true)
->setTarget('_self')
->setCategory('interaction')
->setLink(
$this->generateUrl(
'chamilo_lti_show',
['code' => $course->getCode(), 'id' => $tool->getId()]
)
);
$em->persist($courseTool);
$em->flush();
return $this->redirectToRoute(
'chamilo_course_home_home_index',
['course' => $course->getCode()]
);
}
return $this->redirectToRoute(
'chamilo_lti_configure',
['course' => $course->getCode()]
);
}
}

@ -0,0 +1,135 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\IntegrationBundle\Controller;
use Chamilo\CoreBundle\Controller\BaseController;
use Chamilo\IntegrationBundle\Component\OutcomeDeleteRequest;
use Chamilo\IntegrationBundle\Component\OutcomeReadRequest;
use Chamilo\IntegrationBundle\Component\OutcomeReplaceRequest;
use Chamilo\IntegrationBundle\Component\OutcomeUnsupportedRequest;
use Chamilo\IntegrationBundle\Entity\ExternalTool;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* Class ServicesController.
*
* @package Chamilo\IntegrationBundle\Controller
*/
class ServiceController extends BaseController
{
/**
* @Route("/lti/os", name="chamilo_lti_os")
*
* @param Request $request
*
* @return Response
*/
public function outcomeServiceAction(Request $request): Response
{
$em = $this->getDoctrine()->getManager();
$toolRepo = $em->getRepository('ChamiloIntegrationBundle:ExternalTool');
$headers = \OAuthUtil::get_headers();
if (empty($headers['Authorization'])) {
throw $this->createAccessDeniedException();
}
$authParams = \OAuthUtil::split_header($headers['Authorization']);
if (empty($authParams) || empty($authParams['oauth_consumer_key']) || empty($authParams['oauth_signature'])) {
throw $this->createAccessDeniedException();
}
$course = $this->getCourse();
$tools = $toolRepo->findBy(['consumerKey' => $authParams['oauth_consumer_key']]);
$url = $this->generateUrl('chamilo_lti_os', ['code' => $course->getCode()]);
$toolIsFound = false;
/** @var ExternalTool $tool */
foreach ($tools as $tool) {
$signatureIsValid = $this->compareRequestSignature(
$url,
$authParams['oauth_consumer_key'],
$authParams['oauth_signature'],
$tool
);
if ($signatureIsValid) {
$toolIsFound = true;
break;
}
}
if (!$toolIsFound) {
throw $this->createNotFoundException('External tool not found');
}
$body = file_get_contents('php://input');
$bodyHash = base64_encode(sha1($body, true));
if ($bodyHash !== $authParams['oauth_body_hash']) {
throw $this->createAccessDeniedException('Request is not valid.');
}
$process = $this->processServiceRequest();
$response = new Response($process);
$response->headers->set('Content-Type', 'application/xml');
return $response;
}
/**
* @return \Chamilo\IntegrationBundle\Component\OutcomeResponse|null
*/
private function processServiceRequest()
{
$requestContent = file_get_contents('php://input');
if (empty($requestContent)) {
return null;
}
$xml = new \SimpleXMLElement($requestContent);
if (empty($xml)) {
return null;
}
$bodyChildren = $xml->imsx_POXBody->children();
if (empty($bodyChildren)) {
return null;
}
$name = $bodyChildren->getName();
switch ($name) {
case 'replaceResultRequest':
$serviceRequest = new OutcomeReplaceRequest($xml);
break;
case 'readResultRequest':
$serviceRequest = new OutcomeReadRequest($xml);
break;
case 'deleteResultRequest':
$serviceRequest = new OutcomeDeleteRequest($xml);
break;
default:
$name = str_replace(['ResultRequest', 'Request'], '', $name);
$serviceRequest = new OutcomeUnsupportedRequest($xml, $name);
break;
}
$serviceRequest->setEntityManager($this->getDoctrine()->getManager());
$serviceRequest->setTranslator($this->get('translator'));
return $serviceRequest->process();
}
}

@ -0,0 +1,504 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\IntegrationBundle\Entity;
use Chamilo\CoreBundle\Entity\Course;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* Class ExternalTool.
*
* @package Chamilo\IntegrationBundle\Entity
*
* @ORM\Table(name="lti_external_tool")
* @ORM\Entity()
*/
class ExternalTool
{
/**
* @var integer
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
protected $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string")
*/
private $name = '';
/**
* @var string|null
*
* @ORM\Column(name="description", type="text", nullable=true)
*/
private $description = null;
/**
* @var string
*
* @ORM\Column(name="launch_url", type="string")
*/
private $launchUrl = '';
/**
* @var string
*
* @ORM\Column(name="consumer_key", type="string", nullable=true)
*/
private $consumerKey = '';
/**
* @var string
*
* @ORM\Column(name="shared_secret", type="string", nullable=true)
*/
private $sharedSecret = '';
/**
* @var string|null
*
* @ORM\Column(name="custom_params", type="text", nullable=true)
*/
private $customParams = null;
/**
* @var bool
*
* @ORM\Column(name="active_deep_linking", type="boolean", nullable=false, options={"default": false})
*/
private $activeDeepLinking = false;
/**
* @var null|string
*
* @ORM\Column(name="privacy", type="text", nullable=true, options={"default": null})
*/
private $privacy = null;
/**
* @var Course|null
*
* @ORM\ManyToOne(targetEntity="Chamilo\CoreBundle\Entity\Course")
* @ORM\JoinColumn(name="c_id", referencedColumnName="id")
*/
private $course = null;
/**
* @var GradebookEvaluation|null
*
* @ORM\ManyToOne(targetEntity="Chamilo\CoreBundle\Entity\GradebookEvaluation")
* @ORM\JoinColumn(name="gradebook_eval_id", referencedColumnName="id", onDelete="SET NULL")
*/
private $gradebookEval = null;
/**
* @var ExternalTool|null
*
* @ORM\ManyToOne(targetEntity="Chamilo\IntegrationBundle\Entity\ExternalTool", inversedBy="children")
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id")
*/
private $parent;
/**
* @var ArrayCollection
*
* @ORM\OneToMany(targetEntity="Chamilo\IntegrationBundle\Entity\ExternalTool", mappedBy="parent")
*/
private $children;
/**
* ExternalTool constructor.
*/
public function __construct()
{
$this->description = null;
$this->customParams = null;
$this->activeDeepLinking = false;
$this->course = null;
$this->gradebookEval = null;
$this->privacy = null;
$this->consumerKey = null;
$this->sharedSecret = null;
$this->parent = null;
$this->children = new ArrayCollection();
}
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @param int $id
*
* @return $this
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @param string $name
*
* @return ExternalTool
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* @return null|string
*/
public function getDescription()
{
return $this->description;
}
/**
* @param null|string $description
*
* @return ExternalTool
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* @return string
*/
public function getLaunchUrl()
{
return $this->launchUrl;
}
/**
* @param string $launchUrl
*
* @return ExternalTool
*/
public function setLaunchUrl($launchUrl)
{
$this->launchUrl = $launchUrl;
return $this;
}
/**
* @return string
*/
public function getConsumerKey()
{
return $this->consumerKey;
}
/**
* @param string $consumerKey
*
* @return ExternalTool
*/
public function setConsumerKey($consumerKey)
{
$this->consumerKey = $consumerKey;
return $this;
}
/**
* @return string
*/
public function getSharedSecret()
{
return $this->sharedSecret;
}
/**
* @param string $sharedSecret
*
* @return ExternalTool
*/
public function setSharedSecret($sharedSecret)
{
$this->sharedSecret = $sharedSecret;
return $this;
}
/**
* @return null|string
*/
public function getCustomParams()
{
return $this->customParams;
}
/**
* @param null|string $customParams
*
* @return ExternalTool
*/
public function setCustomParams($customParams)
{
$this->customParams = $customParams;
return $this;
}
/**
* @return bool
*/
public function isGlobal()
{
return $this->course === null;
}
/**
* @param array $params
*
* @return null|string
*/
public function encodeCustomParams(array $params)
{
if (empty($params)) {
return null;
}
$pairs = [];
foreach ($params as $key => $value) {
$pairs[] = "$key=$value";
}
return implode("\n", $pairs);
}
/**
* @return array
*/
public function parseCustomParams()
{
if (empty($this->customParams)) {
return [];
}
$params = [];
$strings = explode("\n", $this->customParams);
foreach ($strings as $string) {
if (empty($string)) {
continue;
}
$pairs = explode('=', $string, 2);
$key = self::parseCustomKey($pairs[0]);
$value = $pairs[1];
$params['custom_'.$key] = $value;
}
return $params;
}
/**
* Map the key from custom param.
*
* @param string $key
*
* @return string
*/
private static function parseCustomKey($key)
{
$newKey = '';
$key = strtolower($key);
$split = str_split($key);
foreach ($split as $char) {
if (
($char >= 'a' && $char <= 'z') || ($char >= '0' && $char <= '9')
) {
$newKey .= $char;
continue;
}
$newKey .= '_';
}
return $newKey;
}
/**
* Get activeDeepLinking.
*
* @return bool
*/
public function isActiveDeepLinking()
{
return $this->activeDeepLinking;
}
/**
* Set activeDeepLinking.
*
* @param bool $activeDeepLinking
*
* @return ExternalTool
*/
public function setActiveDeepLinking($activeDeepLinking)
{
$this->activeDeepLinking = $activeDeepLinking;
return $this;
}
/**
* Get course.
*
* @return Course|null
*/
public function getCourse()
{
return $this->course;
}
/**
* Set course.
*
* @param Course|null $course
*
* @return ExternalTool
*/
public function setCourse(Course $course = null)
{
$this->course = $course;
return $this;
}
/**
* Get gradebookEval.
*
* @return GradebookEvaluation|null
*/
public function getGradebookEval()
{
return $this->gradebookEval;
}
/**
* Set gradebookEval.
*
* @param GradebookEvaluation|null $gradebookEval
*
* @return ExternalTool
*/
public function setGradebookEval($gradebookEval)
{
$this->gradebookEval = $gradebookEval;
return $this;
}
/**
* Get privacy.
*
* @return null|string
*/
public function getPrivacy()
{
return $this->privacy;
}
/**
* Set privacy.
*
* @param bool $shareName
* @param bool $shareEmail
* @param bool $sharePicture
*
* @return ExternalTool
*/
public function setPrivacy($shareName = false, $shareEmail = false, $sharePicture = false)
{
$this->privacy = serialize(
[
'share_name' => $shareName,
'share_email' => $shareEmail,
'share_picture' => $sharePicture,
]
);
return $this;
}
/**
* @return bool
*/
public function isSharingName()
{
$unserialize = $this->unserializePrivacy();
return (bool) $unserialize['share_name'];
}
/**
* @return mixed
*/
public function unserializePrivacy()
{
return unserialize($this->privacy);
}
/**
* @return bool
*/
public function isSharingEmail()
{
$unserialize = $this->unserializePrivacy();
return (bool) $unserialize['share_email'];
}
/**
* @return bool
*/
public function isSharingPicture()
{
$unserialize = $this->unserializePrivacy();
return (bool) $unserialize['share_picture'];
}
/**
* @return ExternalTool|null
*/
public function getParent()
{
return $this->parent;
}
/**
* @param ExternalTool $parent
*
* @return ExternalTool
*/
public function setParent(ExternalTool $parent)
{
$this->parent = $parent;
$this->sharedSecret = $parent->getSharedSecret();
$this->consumerKey = $parent->getConsumerKey();
$this->privacy = $parent->getPrivacy();
return $this;
}
public function __clone()
{
$this->id = 0;
}
}

@ -0,0 +1,176 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\IntegrationBundle\Form;
use Chamilo\IntegrationBundle\Entity\ExternalTool;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class ExternalToolType.
*
* @package Chamilo\IntegrationBundle\Form
*/
class ExternalToolType extends AbstractType
{
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** @var ExternalTool $tool */
$tool = $builder->getData();
$parent = $tool ? $tool->getParent() : null;
$builder
->add('name', TextType::class)
->add('description', TextareaType::class, ['required' => false]);
if (null === $parent) {
$builder
->add('launchUrl', UrlType::class)
->add('consumerKey', TextType::class, ['required' => false])
->add('sharedSecret', TextType::class, ['required' => false]);
}
$builder->add(
'customParams',
TextareaType::class,
[
'required' => false,
'help' => 'Custom params required by the Tool Provider. Format: <code>name=value</code>, one by row.',
]
);
if (null === $parent ||
($parent && !$parent->isActiveDeepLinking())
) {
$builder->add(
'activeDeepLinking',
CheckboxType::class,
[
'label' => 'Support Deep-Linking',
'help' => 'Contact your Tool Provider to verify if Deep Linking support is mandatory',
'required' => false,
]
);
}
$builder
->add(
'shareName',
CheckboxType::class,
['mapped' => false, 'help' => "Share launcher's name", 'required' => false]
)
->add(
'shareEmail',
CheckboxType::class,
['mapped' => false, 'help' => "Share launcher's email", 'required' => false]
)
->add(
'sharePicture',
CheckboxType::class,
['mapped' => false, 'help' => "Share launcher's picture", 'required' => false]
);
$builder->add(
empty($tool->getId()) ? 'save' : 'edit',
SubmitType::class,
['attr' => ['class' => 'btn btn-primary']]
);
$builder->addEventListener(
FormEvents::POST_SUBMIT,
[$this, 'onPostSubmit']
);
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class', ExternalTool::class]);
}
/**
* @param FormEvent $event
*/
public function onPostSubmit(FormEvent $event)
{
/** @var ExternalTool $tool */
$tool = $event->getData();
$form = $event->getForm();
if (!$tool) {
return;
}
$tool->setPrivacy(
$form->get('shareName')->getData(),
$form->get('shareEmail')->getData(),
$form->get('sharePicture')->getData()
);
$cartridgeUrl = $this->getLaunchUrlFromCartridge($tool->getLaunchUrl());
if (!empty($cartridgeUrl)) {
$tool->setLaunchUrl($cartridgeUrl);
}
}
/**
* @param string $launchUrl
*
* @return string|null
*/
private function getLaunchUrlFromCartridge($launchUrl)
{
$options = [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_POST => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_ENCODING => '',
CURLOPT_SSL_VERIFYPEER => false,
];
$ch = curl_init($launchUrl);
curl_setopt_array($ch, $options);
$content = curl_exec($ch);
$errno = curl_errno($ch);
curl_close($ch);
if ($errno !== 0) {
return null;
}
libxml_use_internal_errors(true);
$sxe = simplexml_load_string($content);
if (false === $sxe) {
return null;
}
$xml = new \SimpleXMLElement($content);
$result = $xml->xpath('blti:launch_url');
if (empty($result)) {
return null;
}
$launchUrl = $result[0];
return (string)$launchUrl;
}
}

@ -0,0 +1,14 @@
chamilo_lti_admin_controller:
resource: '@ChamiloIntegrationBundle/Controller/AdminController.php'
type: annotation
prefix: '/admin/lti'
chamilo_lti_teacher_controller:
resource: '@ChamiloIntegrationBundle/Controller/CourseController.php'
type: annotation
prefix: '/courses/{code}/lti'
chamilo_lti_service_controller:
path: /lti/os
defaults:
_controller: 'ChamiloIntegrationBundle:Service:outcomeService'

@ -0,0 +1,62 @@
{% extends '@ChamiloTheme/Layout/layout_one_col.html.twig' %}
{% block content %}
<section class="bg-white p-4">
<div class="row">
<div class="col-12">
<h2>{{ 'External tools'|trans }}</h2>
<p>
{{ 'LTI intro tool'|trans }}
</p>
<div class="btn-toolbar" role="toolbar" aria-label="{{ 'LTI Toolbar'|trans }}">
<a href="{{ url('chamilo_lti_admin_add') }}" foo="{{ _p.web_plugin }}ims_lti/create.php"
class="btn btn-primary">
<span class="fa fa-plus fa-fw" aria-hidden="true"></span> {{ 'Add external tool'|trans }}
</a>
</div>
<div class="table-responsive">
<table class="table table-hover table-striped">
<thead>
<tr>
<th>{{ 'Name'|trans }}</th>
<th>{{ 'Launch URL'|trans }}</th>
<th class="text-center">{{ 'Is global'|trans }}</th>
<th class="text-right">{{ 'Actions'|trans }}</th>
</tr>
</thead>
<tbody>
{% for tool in tools %}
<tr>
<td>{{ tool.name }}</td>
<td>{{ tool.launchUrl }}</td>
<td class="text-center">
{% if tool.isGlobal() %}
<span class="fa fa-check-square" aria-hidden="true"></span>
<span class="sr-only">{{ 'Yes'|trans }}</span>
{% else %}
<span class="fa fa-square" aria-hidden="true"></span>
<span class="sr-only">{{ 'No'|trans }}</span>
{% endif %}
</td>
<td class="text-right">
<a href="{{ url('chamilo_lti_admin_edit', {'toolId': tool.id}) }}"
class="btn btn-success mb-1 mb-lg-0">
<span class="fa fa-edit fa-fw" aria-hidden="true"></span>
<span class="sr-only">{{ 'Edit'|trans }}</span>
</a>
<a href="{{ url('chamilo_lti_admin_delete', {'toolId': tool.id}) }}"
class="btn btn-danger">
<span class="fa fa-times fa-fw" aria-hidden="true"></span>
<span class="sr-only">{{ 'Delete'|trans }}</span>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</section>
{% endblock %}

@ -0,0 +1,14 @@
{% extends '@ChamiloTheme/Layout/layout_one_col.html.twig' %}
{% block content %}
<section class="bg-white p-4">
<div class="row">
<div class="col-12">
<h3>{{ 'External tool settings'|trans }}</h3>
{{ form(form) }}
</div>
</div>
</section>
{% endblock %}

@ -0,0 +1,70 @@
{% extends 'ChamiloThemeBundle:Layout:layout_one_col.html.twig' %}
{% block content %}
<section class="bg-white p-4">
<div class="row">
<div class="col-md-4">
{% if not added_tools is empty %}
<div class="card bg-light mb-4">
<h5 class="card-header">{{ 'Tools added'|trans }}</h5>
<ul class="list-group list-group-flush">
{% for tool in added_tools %}
<li class="list-group-item">
<div class="float-right">
{% if tool.isActiveDeepLinking %}
<a href="{{ url('chamilo_lti_show', {"id": tool.id, "code": course.code}) }}"
class="btn btn-light btn-sm">
<span class="fa fa-rocket" aria-hidden="true"></span>
<span class="sr-only">{{ 'Configure'|trans }}</span>
</a>
{% endif %}
<a href="{{ url('chamilo_lti_edit', {"id": tool.id, "code": course.code}) }}"
class="btn btn-light btn-sm">
<span class="fa fa-edit" aria-hidden="true"></span>
<span class="sr-only">{{ 'Edit'|trans }}</span>
</a>
</div>
{{ tool.name }}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if not global_tools is empty %}
<div class="card bg-light">
<h5 class="card-header">{{ 'Available tools'|trans }}</h5>
<ul class="list-group list-group-flush">
{% for tool in global_tools %}
<li class="list-group-item">
<div class="float-right">
{% if tool.isActiveDeepLinking %}
<a href="{{ url('chamilo_lti_launch', {id: tool.id}) }}">
<span class="fa fa-rocket" aria-hidden="true"></span>
<span class="sr-only">{{ 'Configure'|trans }}</span>
</a>
{% else %}
<a href="{{ url('chamilo_lti_configure_global', {"code": course.code, "id": tool.id}) }}"
class="btn btn-light btn-sm">
<span class="fa fa-plus" aria-hidden="true"></span>
<span class="sr-only">{{ 'Add'|trans }}</span>
</a>
{% endif %}
</div>
{{ tool.name }}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
<div class="col-md-8">
<h3>{{ title }}</h3>
{{ form(form) }}
</div>
</div>
</section>
{% endblock %}

@ -0,0 +1,20 @@
{% extends '@ChamiloTheme/Layout/layout_one_col.html.twig' %}
{% block content %}
<section class="bg-white">
{% if not tool.description is empty %}
{% if tool.parent.activeDeepLinking %}
{% autoescape false %}
<div class="m-3 pt-3">{{ tool.description }}</div>
{% endautoescape %}
{% else %}
<p class="m-3 pt-3">{{ tool.description }}</p>
{% endif %}
{% endif %}
<div class="embed-responsive embed-responsive-16by9">
<iframe class="embed-responsive-item plugin-ims-lti-iframe" allowfullscreen
src="{{ url('chamilo_lti_launch', {'code': course.code, 'id': tool.id}) }}"></iframe>
</div>
</section>
{% endblock %}

@ -0,0 +1,7 @@
{% extends '@ChamiloTheme/Layout/blank.html.twig' %}
{% block content %}
<script>
window.parent.location.href = '{{ url('chamilo_course_home_home_index', {'course': course.code}) }}';
</script>
{% endblock %}

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>LTI Launch</title>
</head>
<body>
<form action="{{ launch_url }}" name="ltiLaunchForm" method="post" encType="application/x-www-form-urlencoded">
{% for key, value in params %}
<input type="hidden" name="{{ key }}" value="{{ value }}">
{% endfor %}
</form>
<script>document.ltiLaunchForm.submit();</script>
</body>
</html>
Loading…
Cancel
Save