ImsLtiPlugin.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. <?php
  2. /* For license terms, see /license.txt */
  3. use Chamilo\CoreBundle\Entity\Course;
  4. use Chamilo\CoreBundle\Entity\CourseRelUser;
  5. use Chamilo\CoreBundle\Entity\Session;
  6. use Chamilo\CoreBundle\Entity\SessionRelCourseRelUser;
  7. use Chamilo\CourseBundle\Entity\CTool;
  8. use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
  9. use Chamilo\UserBundle\Entity\User;
  10. use Doctrine\DBAL\DBALException;
  11. use Doctrine\DBAL\Schema\Schema;
  12. use Doctrine\DBAL\Types\Type;
  13. use Symfony\Component\Filesystem\Filesystem;
  14. /**
  15. * Description of MsiLti
  16. *
  17. * @author Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>
  18. */
  19. class ImsLtiPlugin extends Plugin
  20. {
  21. const TABLE_TOOL = 'plugin_ims_lti_tool';
  22. public $isAdminPlugin = true;
  23. /**
  24. * Class constructor
  25. */
  26. protected function __construct()
  27. {
  28. $version = '1.5.1 (beta)';
  29. $author = 'Angel Fernando Quiroz Campos';
  30. parent::__construct($version, $author, ['enabled' => 'boolean']);
  31. $this->setCourseSettings();
  32. }
  33. /**
  34. * Get the class instance
  35. * @staticvar MsiLtiPlugin $result
  36. * @return ImsLtiPlugin
  37. */
  38. public static function create()
  39. {
  40. static $result = null;
  41. return $result ?: $result = new self();
  42. }
  43. /**
  44. * Get the plugin directory name
  45. */
  46. public function get_name()
  47. {
  48. return 'ims_lti';
  49. }
  50. /**
  51. * Install the plugin. Setup the database
  52. */
  53. public function install()
  54. {
  55. $pluginEntityPath = $this->getEntityPath();
  56. if (!is_dir($pluginEntityPath)) {
  57. if (!is_writable(dirname($pluginEntityPath))) {
  58. $message = get_lang('ErrorCreatingDir').': '.$pluginEntityPath;
  59. Display::addFlash(Display::return_message($message, 'error'));
  60. return false;
  61. }
  62. mkdir($pluginEntityPath, api_get_permissions_for_new_directories());
  63. }
  64. $fs = new Filesystem();
  65. $fs->mirror(__DIR__.'/Entity/', $pluginEntityPath, null, ['override']);
  66. $this->createPluginTables();
  67. }
  68. /**
  69. * Unistall plugin. Clear the database
  70. */
  71. public function uninstall()
  72. {
  73. $pluginEntityPath = $this->getEntityPath();
  74. $fs = new Filesystem();
  75. if ($fs->exists($pluginEntityPath)) {
  76. $fs->remove($pluginEntityPath);
  77. }
  78. try {
  79. $this->dropPluginTables();
  80. $this->removeTools();
  81. } catch (DBALException $e) {
  82. error_log('Error while uninstalling IMS/LTI plugin: '.$e->getMessage());
  83. }
  84. }
  85. /**
  86. * Creates the plugin tables on database
  87. *
  88. * @return boolean
  89. * @throws DBALException
  90. */
  91. private function createPluginTables()
  92. {
  93. $entityManager = Database::getManager();
  94. $connection = $entityManager->getConnection();
  95. if ($connection->getSchemaManager()->tablesExist(self::TABLE_TOOL)) {
  96. return true;
  97. }
  98. $queries = [
  99. 'CREATE TABLE '.self::TABLE_TOOL.' (
  100. id INT AUTO_INCREMENT NOT NULL,
  101. c_id INT DEFAULT NULL,
  102. gradebook_eval_id INT DEFAULT NULL,
  103. parent_id INT DEFAULT NULL,
  104. name VARCHAR(255) NOT NULL,
  105. description LONGTEXT DEFAULT NULL,
  106. launch_url VARCHAR(255) NOT NULL,
  107. consumer_key VARCHAR(255) DEFAULT NULL,
  108. shared_secret VARCHAR(255) DEFAULT NULL,
  109. custom_params LONGTEXT DEFAULT NULL,
  110. active_deep_linking TINYINT(1) DEFAULT \'0\' NOT NULL,
  111. privacy LONGTEXT DEFAULT NULL,
  112. INDEX IDX_C5E47F7C91D79BD3 (c_id),
  113. INDEX IDX_C5E47F7C82F80D8B (gradebook_eval_id),
  114. INDEX IDX_C5E47F7C727ACA70 (parent_id),
  115. PRIMARY KEY(id)
  116. ) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB',
  117. 'ALTER TABLE '.self::TABLE_TOOL.' ADD CONSTRAINT FK_C5E47F7C91D79BD3
  118. FOREIGN KEY (c_id) REFERENCES course (id)',
  119. 'ALTER TABLE '.self::TABLE_TOOL.' ADD CONSTRAINT FK_C5E47F7C82F80D8B
  120. FOREIGN KEY (gradebook_eval_id) REFERENCES gradebook_evaluation (id) ON DELETE SET NULL',
  121. 'ALTER TABLE '.self::TABLE_TOOL.' ADD CONSTRAINT FK_C5E47F7C727ACA70
  122. FOREIGN KEY (parent_id) REFERENCES '.self::TABLE_TOOL.' (id) ON DELETE CASCADE;',
  123. ];
  124. foreach ($queries as $query) {
  125. Database::query($query);
  126. }
  127. return true;
  128. }
  129. /**
  130. * Drops the plugin tables on database
  131. *
  132. * @return boolean
  133. */
  134. private function dropPluginTables()
  135. {
  136. $entityManager = Database::getManager();
  137. $connection = $entityManager->getConnection();
  138. $chamiloSchema = $connection->getSchemaManager();
  139. if (!$chamiloSchema->tablesExist([self::TABLE_TOOL])) {
  140. return false;
  141. }
  142. $sql = 'DROP TABLE IF EXISTS '.self::TABLE_TOOL;
  143. Database::query($sql);
  144. return true;
  145. }
  146. /**
  147. *
  148. */
  149. private function removeTools()
  150. {
  151. $sql = "DELETE FROM c_tool WHERE link LIKE 'ims_lti/start.php%' AND category = 'plugin'";
  152. Database::query($sql);
  153. }
  154. /**
  155. * Set the course settings
  156. */
  157. private function setCourseSettings()
  158. {
  159. $button = Display::toolbarButton(
  160. $this->get_lang('ConfigureExternalTool'),
  161. api_get_path(WEB_PLUGIN_PATH).'ims_lti/configure.php?'.api_get_cidreq(),
  162. 'cog',
  163. 'primary'
  164. );
  165. $this->course_settings = [
  166. [
  167. 'name' => $this->get_lang('ImsLtiDescription').$button.'<hr>',
  168. 'type' => 'html',
  169. ],
  170. ];
  171. }
  172. /**
  173. * @param Course $course
  174. * @param ImsLtiTool $ltiTool
  175. *
  176. * @return CTool
  177. */
  178. public function findCourseToolByLink(Course $course, ImsLtiTool $ltiTool)
  179. {
  180. $em = Database::getManager();
  181. $toolRepo = $em->getRepository('ChamiloCourseBundle:CTool');
  182. /** @var CTool $cTool */
  183. $cTool = $toolRepo->findOneBy(
  184. [
  185. 'cId' => $course,
  186. 'link' => self::generateToolLink($ltiTool),
  187. ]
  188. );
  189. return $cTool;
  190. }
  191. /**
  192. * @param CTool $courseTool
  193. * @param ImsLtiTool $ltiTool
  194. *
  195. * @throws \Doctrine\ORM\OptimisticLockException
  196. */
  197. public function updateCourseTool(CTool $courseTool, ImsLtiTool $ltiTool)
  198. {
  199. $em = Database::getManager();
  200. $courseTool->setName($ltiTool->getName());
  201. $em->persist($courseTool);
  202. $em->flush();
  203. }
  204. /**
  205. * @param ImsLtiTool $tool
  206. *
  207. * @return string
  208. */
  209. private static function generateToolLink(ImsLtiTool $tool)
  210. {
  211. return 'ims_lti/start.php?id='.$tool->getId();
  212. }
  213. /**
  214. * Add the course tool
  215. *
  216. * @param Course $course
  217. * @param ImsLtiTool $tool
  218. */
  219. public function addCourseTool(Course $course, ImsLtiTool $tool)
  220. {
  221. $this->createLinkToCourseTool(
  222. $tool->getName(),
  223. $course->getId(),
  224. null,
  225. self::generateToolLink($tool)
  226. );
  227. }
  228. /**
  229. * @return string
  230. */
  231. protected function getConfigExtraText()
  232. {
  233. $text = $this->get_lang('ImsLtiDescription');
  234. $text .= sprintf(
  235. $this->get_lang('ManageToolButton'),
  236. api_get_path(WEB_PLUGIN_PATH).'ims_lti/admin.php'
  237. );
  238. return $text;
  239. }
  240. /**
  241. * @return string
  242. */
  243. public function getEntityPath()
  244. {
  245. return api_get_path(SYS_PATH).'src/Chamilo/PluginBundle/Entity/'.$this->getCamelCaseName();
  246. }
  247. public static function isInstructor()
  248. {
  249. api_is_allowed_to_edit(false, true);
  250. }
  251. /**
  252. * @param User $user
  253. *
  254. * @return string
  255. */
  256. public static function getUserRoles(User $user)
  257. {
  258. if (DRH === $user->getStatus()) {
  259. return 'urn:lti:role:ims/lis/Mentor';
  260. }
  261. if ($user->getStatus() === INVITEE) {
  262. return 'Learner,urn:lti:role:ims/lis/Learner/GuestLearner';
  263. }
  264. if (!api_is_allowed_to_edit(false, true)) {
  265. return 'Learner';
  266. }
  267. $roles = ['Instructor'];
  268. if (api_is_platform_admin_by_id($user->getId())) {
  269. $roles[] = 'urn:lti:role:ims/lis/Administrator';
  270. }
  271. return implode(',', $roles);
  272. }
  273. /**
  274. * @param int $userId
  275. *
  276. * @return string
  277. */
  278. public static function generateToolUserId($userId)
  279. {
  280. $siteName = api_get_setting('siteName');
  281. $institution = api_get_setting('Institution');
  282. $toolUserId = "$siteName - $institution - $userId";
  283. $toolUserId = api_replace_dangerous_char($toolUserId);
  284. return $toolUserId;
  285. }
  286. /**
  287. * @param User $currentUser
  288. *
  289. * @return string
  290. */
  291. public static function getRoleScopeMentor(User $currentUser)
  292. {
  293. if (DRH !== $currentUser->getStatus()) {
  294. return '';
  295. }
  296. $followedUsers = UserManager::get_users_followed_by_drh($currentUser->getId());
  297. $scope = [];
  298. foreach ($followedUsers as $userInfo) {
  299. $scope[] = self::generateToolUserId($userInfo['user_id']);
  300. }
  301. return implode(',', $scope);
  302. }
  303. /**
  304. * @param array $contentItem
  305. * @param ImsLtiTool $baseLtiTool
  306. * @param Course $course
  307. *
  308. * @throws \Doctrine\ORM\OptimisticLockException
  309. */
  310. public function saveItemAsLtiLink(array $contentItem, ImsLtiTool $baseLtiTool, Course $course)
  311. {
  312. $em = Database::getManager();
  313. $ltiToolRepo = $em->getRepository('ChamiloPluginBundle:ImsLti\ImsLtiTool');
  314. $url = empty($contentItem['url']) ? $baseLtiTool->getLaunchUrl() : $contentItem['url'];
  315. /** @var ImsLtiTool $newLtiTool */
  316. $newLtiTool = $ltiToolRepo->findOneBy(['launchUrl' => $url, 'parent' => $baseLtiTool, 'course' => $course]);
  317. if (null === $newLtiTool) {
  318. $newLtiTool = new ImsLtiTool();
  319. $newLtiTool
  320. ->setLaunchUrl($url)
  321. ->setParent(
  322. $baseLtiTool
  323. )
  324. ->setPrivacy(
  325. $baseLtiTool->isSharingName(),
  326. $baseLtiTool->isSharingEmail(),
  327. $baseLtiTool->isSharingPicture()
  328. )
  329. ->setCourse($course);
  330. }
  331. $newLtiTool
  332. ->setName(
  333. !empty($contentItem['title']) ? $contentItem['title'] : $baseLtiTool->getName()
  334. )
  335. ->setDescription(
  336. !empty($contentItem['text']) ? $contentItem['text'] : null
  337. );
  338. if (!empty($contentItem['custom'])) {
  339. $newLtiTool
  340. ->setCustomParams(
  341. $newLtiTool->encodeCustomParams($contentItem['custom'])
  342. );
  343. }
  344. $em->persist($newLtiTool);
  345. $em->flush();
  346. $courseTool = $this->findCourseToolByLink($course, $newLtiTool);
  347. if ($courseTool) {
  348. $this->updateCourseTool($courseTool, $newLtiTool);
  349. return;
  350. }
  351. $this->addCourseTool($course, $newLtiTool);
  352. }
  353. /**
  354. * @return null|SimpleXMLElement
  355. */
  356. private function getRequestXmlElement()
  357. {
  358. $request = file_get_contents("php://input");
  359. if (empty($request)) {
  360. return null;
  361. }
  362. $xml = new SimpleXMLElement($request);
  363. return $xml;
  364. }
  365. /**
  366. * @return ImsLtiServiceResponse|null
  367. */
  368. public function processServiceRequest()
  369. {
  370. $xml = $this->getRequestXmlElement();
  371. if (empty($xml)) {
  372. return null;
  373. }
  374. $request = ImsLtiServiceRequestFactory::create($xml);
  375. $response = $request->process();
  376. return $response;
  377. }
  378. /**
  379. * @param int $toolId
  380. * @param Course $course
  381. *
  382. * @return bool
  383. */
  384. public static function existsToolInCourse($toolId, Course $course)
  385. {
  386. $em = Database::getManager();
  387. $toolRepo = $em->getRepository('ChamiloPluginBundle:ImsLti\ImsLtiTool');
  388. /** @var ImsLtiTool $tool */
  389. $tool = $toolRepo->findOneBy(['id' => $toolId, 'course' => $course]);
  390. return !empty($tool);
  391. }
  392. /**
  393. * @param string $configUrl
  394. *
  395. * @return string
  396. * @throws Exception
  397. */
  398. public function getLaunchUrlFromCartridge($configUrl)
  399. {
  400. $options = [
  401. CURLOPT_CUSTOMREQUEST => 'GET',
  402. CURLOPT_POST => false,
  403. CURLOPT_RETURNTRANSFER => true,
  404. CURLOPT_HEADER => false,
  405. CURLOPT_FOLLOWLOCATION => true,
  406. CURLOPT_ENCODING => '',
  407. CURLOPT_SSL_VERIFYPEER => false,
  408. ];
  409. $ch = curl_init($configUrl);
  410. curl_setopt_array($ch, $options);
  411. $content = curl_exec($ch);
  412. $errno = curl_errno($ch);
  413. curl_close($ch);
  414. if ($errno !== 0) {
  415. throw new Exception($this->get_lang('NoAccessToUrl'));
  416. }
  417. $xml = new SimpleXMLElement($content);
  418. $result = $xml->xpath('blti:launch_url');
  419. if (empty($result)) {
  420. throw new Exception($this->get_lang('LaunchUrlNotFound'));
  421. }
  422. $launchUrl = $result[0];
  423. return (string) $launchUrl;
  424. }
  425. /**
  426. * @param array $params
  427. */
  428. public function trimParams(array &$params)
  429. {
  430. foreach ($params as $key => $value) {
  431. $newValue = preg_replace('/\s+/', ' ', $value);
  432. $params[$key] = trim($newValue);
  433. }
  434. }
  435. /**
  436. * @param ImsLtiTool $tool
  437. * @param array $params
  438. *
  439. * @return array
  440. */
  441. public function removeUrlParamsFromLaunchParams(ImsLtiTool $tool, array &$params)
  442. {
  443. $urlQuery = parse_url($tool->getLaunchUrl(), PHP_URL_QUERY);
  444. if (empty($urlQuery)) {
  445. return $params;
  446. }
  447. $queryParams = [];
  448. parse_str($urlQuery, $queryParams);
  449. $queryKeys = array_keys($queryParams);
  450. foreach ($queryKeys as $key) {
  451. if (isset($params[$key])) {
  452. unset($params[$key]);
  453. }
  454. }
  455. }
  456. /**
  457. * Avoid conflict with foreign key when deleting a course
  458. *
  459. * @param int $courseId
  460. */
  461. public function doWhenDeletingCourse($courseId)
  462. {
  463. $em = Database::getManager();
  464. $q = $em
  465. ->createQuery(
  466. 'DELETE FROM ChamiloPluginBundle:ImsLti\ImsLtiTool tool
  467. WHERE tool.course = :c_id and tool.parent IS NOT NULL'
  468. );
  469. error_log($q->getSQL());
  470. $q->execute(['c_id' => (int) $courseId]);
  471. $em->createQuery('DELETE FROM ChamiloPluginBundle:ImsLti\ImsLtiTool tool WHERE tool.course = :c_id')
  472. ->execute(['c_id' => (int) $courseId]);
  473. }
  474. }