parent
b08507cf4e
commit
6d643758e7
@ -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…
Reference in new issue