diff --git a/plugin/xapi/cmi5/add.php b/plugin/xapi/cmi5/add.php deleted file mode 100644 index 4df8ce6022..0000000000 --- a/plugin/xapi/cmi5/add.php +++ /dev/null @@ -1,119 +0,0 @@ -addFile('file', $plugin->get_lang('Cmi5Package')); -$frmActivity->addButtonAdvancedSettings('lrs_params', $plugin->get_lang('LrsConfiguration')); -$frmActivity->addHtml(''); -$frmActivity->addButtonImport(get_lang('Import')); -$frmActivity->addRule('file', get_lang('ThisFileIsRequired'), 'required'); -$frmActivity->addRule( - 'file', - $plugin->get_lang('OnlyZipAllowed'), - 'filetype', - ['zip'] -); -$frmActivity->applyFilter('title', 'trim'); -$frmActivity->applyFilter('description', 'trim'); -$frmActivity->applyFilter('lrs_url', 'trim'); -$frmActivity->applyFilter('lrs_auth', 'trim'); - -if ($frmActivity->validate()) { - $values = $frmActivity->exportValues(); - $zipFileInfo = $_FILES['file']; - - try { - $packageFile = Cmi5Importer::create($zipFileInfo, $course)->import(); - - $parser = Cmi5Parser::create($packageFile, $course, $session); - - $toolLaunch = $parser->parse(); - } catch (Exception $e) { - Display::addFlash( - Display::return_message($e->getMessage(), 'error') - ); - - exit; - } - - if (!empty($values['lrs_url']) - && !empty($values['lrs_auth_username']) - && !empty($values['lrs_auth_password']) - ) { - $toolLaunch - ->setLrsUrl($values['lrs_url']) - ->setLrsAuthUsername($values['lrs_auth_username']) - ->setLrsAuthUsername($values['lrs_auth_password']); - } - - $em = Database::getManager(); - $em->persist($toolLaunch); - - foreach ($parser->getToc() as $cmi5Item) { - $cmi5Item->setTool($toolLaunch); - - $em->persist($cmi5Item); - } - - $em->flush(); - - Display::addFlash( - Display::return_message($plugin->get_lang('ActivityImported'), 'success') - ); - - header('Location: '.api_get_course_url()); - exit; -} - -$frmActivity->setDefaults(['allow_multiple_attempts' => true]); - -$pageTitle = $plugin->get_title(); -$pageContent = $frmActivity->returnForm(); - -$interbreadcrumb[] = ['url' => '../tincan/index.php', 'name' => $plugin->get_lang('ToolTinCan')]; - -$langAddActivity = $plugin->get_lang('AddActivity'); - -$view = new Template($langAddActivity); -$view->assign('header', $langAddActivity); -$view->assign('content', $pageContent); -$view->display_one_col_template(); diff --git a/plugin/xapi/cmi5/launch_item.php b/plugin/xapi/cmi5/launch.php similarity index 100% rename from plugin/xapi/cmi5/launch_item.php rename to plugin/xapi/cmi5/launch.php diff --git a/plugin/xapi/cmi5/tool.php b/plugin/xapi/cmi5/view.php similarity index 96% rename from plugin/xapi/cmi5/tool.php rename to plugin/xapi/cmi5/view.php index 5c45add495..dc0f083359 100644 --- a/plugin/xapi/cmi5/tool.php +++ b/plugin/xapi/cmi5/view.php @@ -62,7 +62,7 @@ $tocHtml = $itemsRepo->buildTree( return Display::url( $title, - "launch_item.php?tool={$toolLaunch->getId()}&id={$node['id']}&$cidReq", + "launch.php?tool={$toolLaunch->getId()}&id={$node['id']}&$cidReq", [ 'target' => 'ifr_content', 'class' => 'text-left btn-link', diff --git a/plugin/xapi/src/Entity/Cmi5Item.php b/plugin/xapi/src/Entity/Cmi5Item.php index 3980c70cf3..c3f86deea0 100644 --- a/plugin/xapi/src/Entity/Cmi5Item.php +++ b/plugin/xapi/src/Entity/Cmi5Item.php @@ -100,7 +100,7 @@ class Cmi5Item /** * @var \Chamilo\PluginBundle\Entity\XApi\ToolLaunch * - * @ORM\ManyToOne(targetEntity="Chamilo\PluginBundle\Entity\XApi\ToolLaunch") + * @ORM\ManyToOne(targetEntity="Chamilo\PluginBundle\Entity\XApi\ToolLaunch", inversedBy="items") * @ORM\JoinColumn(name="tool_id", referencedColumnName="id", onDelete="CASCADE") */ private $tool; diff --git a/plugin/xapi/src/Entity/ToolLaunch.php b/plugin/xapi/src/Entity/ToolLaunch.php index 28ee0d2179..6b07776841 100644 --- a/plugin/xapi/src/Entity/ToolLaunch.php +++ b/plugin/xapi/src/Entity/ToolLaunch.php @@ -6,6 +6,7 @@ namespace Chamilo\PluginBundle\Entity\XApi; use Chamilo\CoreBundle\Entity\Course; use Chamilo\CoreBundle\Entity\Session; use DateTime; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; /** @@ -100,6 +101,13 @@ class ToolLaunch * @ORM\Column(name="created_at", type="datetime") */ private $createdAt; + /** + * @var \Doctrine\Common\Collections\ArrayCollection + * + * @ORM\OneToMany(targetEntity="Chamilo\PluginBundle\Entity\XApi\Cmi5Item", mappedBy="tool", orphanRemoval=true, + * cascade="ALL") + */ + private $items; /** * ToolLaunch constructor. @@ -107,6 +115,7 @@ class ToolLaunch public function __construct() { $this->allowMultipleAttempts = true; + $this->items = new ArrayCollection(); } public function getId(): int @@ -264,4 +273,26 @@ class ToolLaunch return $this; } + + /** + * @return \Doctrine\Common\Collections\ArrayCollection + */ + public function getItems(): ArrayCollection + { + return $this->items; + } + + /** + * @param \Chamilo\PluginBundle\Entity\XApi\Cmi5Item $cmi5Item + * + * @return $this + */ + public function addItem(Cmi5Item $cmi5Item) + { + $cmi5Item->setTool($this); + + $this->items->add($cmi5Item); + + return $this; + } } diff --git a/plugin/xapi/src/Importer/AbstractImporter.php b/plugin/xapi/src/Importer/AbstractImporter.php deleted file mode 100644 index 449f11061a..0000000000 --- a/plugin/xapi/src/Importer/AbstractImporter.php +++ /dev/null @@ -1,129 +0,0 @@ -course = $course; - - $pathInfo = pathinfo($fileInfo['name']); - - $this->courseDirectoryPath = api_get_path(SYS_COURSE_PATH).$this->course->getDirectory(); - $this->toolDirectory = $toolDirectory; - $this->packageDirectoryPath = implode( - '/', - [ - $this->courseDirectoryPath, - $this->toolDirectory, - api_replace_dangerous_char($pathInfo['filename']), - ] - ); - - $this->zipFile = new PclZip($fileInfo['tmp_name']); - } - - /** - * @return \Chamilo\PluginBundle\XApi\Importer\AbstractImporter - */ - abstract public static function create(array $fileInfo, Course $course); - - /** - * @throws \Exception - */ - public function import() - { - $this->validPackage(); - - if (!$this->isEnoughSpace()) { - throw new Exception('Not enough space to storage package.'); - } - - $fs = new Filesystem(); - $fs->mkdir( - $this->packageDirectoryPath, - api_get_permissions_for_new_directories() - ); - - $this->zipFile->extract($this->packageDirectoryPath); - - return "{$this->packageDirectoryPath}/{$this->toolDirectory}.xml"; - } - - /** - * @throws \Exception - */ - protected function validPackage() - { - $zipContent = $this->zipFile->listContent(); - - if (empty($zipContent)) { - throw new Exception('Package file is empty'); - } - - foreach ($zipContent as $zipEntry) { - if (preg_match('~.(php.*|phtml)$~i', $zipEntry['filename'])) { - throw new Exception("File \"{$zipEntry['filename']}\" contains a PHP script"); - } - } - } - - /** - * @return bool - */ - private function isEnoughSpace() - { - $zipRealSize = array_reduce( - $this->zipFile->listContent(), - function ($accumulator, $zipEntry) { - return $accumulator + $zipEntry['size']; - } - ); - - $courseSpaceQuota = DocumentManager::get_course_quota($this->course->getCode()); - - if (!enough_size($zipRealSize, $this->courseDirectoryPath, $courseSpaceQuota)) { - return false; - } - - return true; - } -} diff --git a/plugin/xapi/src/Importer/Cmi5Importer.php b/plugin/xapi/src/Importer/Cmi5Importer.php deleted file mode 100644 index 3ab5f5fcb7..0000000000 --- a/plugin/xapi/src/Importer/Cmi5Importer.php +++ /dev/null @@ -1,45 +0,0 @@ -zipFile->listContent(); - - $isValid = false; - - foreach ($zipContent as $zipEntry) { - if ('cmi5.xml' === $zipEntry['filename']) { - $isValid = true; - } - } - - if (!$isValid) { - throw new \Exception('Incorrect package. Missing "cmi5.xml" file'); - } - } -} diff --git a/plugin/xapi/src/Importer/PackageImporter.php b/plugin/xapi/src/Importer/PackageImporter.php new file mode 100644 index 0000000000..db9a59d19e --- /dev/null +++ b/plugin/xapi/src/Importer/PackageImporter.php @@ -0,0 +1,76 @@ +packageFileInfo = $fileInfo; + $this->course = $course; + + $this->courseDirectoryPath = api_get_path(SYS_COURSE_PATH).$this->course->getDirectory(); + } + + /** + * @param array $fileInfo + * @param \Chamilo\CoreBundle\Entity\Course $course + * + * @return \Chamilo\PluginBundle\XApi\Importer\XmlPackageImporter|\Chamilo\PluginBundle\XApi\Importer\ZipPackageImporter + */ + public static function create(array $fileInfo, Course $course) + { + if ('text/xml' === $fileInfo['type']) { + return new XmlPackageImporter($fileInfo, $course); + } + + return new ZipPackageImporter($fileInfo, $course); + } + + /** + * @throws \Exception + * + * @return mixed + */ + abstract public function import(): string; + + /** + * @return string + */ + public function getPackageType(): string + { + return $this->packageType; + } +} diff --git a/plugin/xapi/src/Importer/TinCanImporter.php b/plugin/xapi/src/Importer/TinCanImporter.php deleted file mode 100644 index 3e3b4fccac..0000000000 --- a/plugin/xapi/src/Importer/TinCanImporter.php +++ /dev/null @@ -1,46 +0,0 @@ -zipFile->listContent(); - - $isValid = false; - - foreach ($zipContent as $zipEntry) { - if ('tincan.xml' === $zipEntry['filename']) { - $isValid = true; - } - } - - if (!$isValid) { - throw new Exception('Incorrect package. Missing "tincan.xml" file'); - } - } -} diff --git a/plugin/xapi/src/Importer/XmlPackageImporter.php b/plugin/xapi/src/Importer/XmlPackageImporter.php new file mode 100644 index 0000000000..6350c484d0 --- /dev/null +++ b/plugin/xapi/src/Importer/XmlPackageImporter.php @@ -0,0 +1,29 @@ +packageFileInfo['name'], ['tincan.xml', 'cmi5.xml'])) { + throw new Exception('Invalid package'); + } + + $this->packageType = explode('.', $this->packageFileInfo['name'], 2)[0]; + + return $this->packageFileInfo['tmp_name']; + } +} diff --git a/plugin/xapi/src/Importer/ZipPackageImporter.php b/plugin/xapi/src/Importer/ZipPackageImporter.php new file mode 100644 index 0000000000..5dcfb095a9 --- /dev/null +++ b/plugin/xapi/src/Importer/ZipPackageImporter.php @@ -0,0 +1,95 @@ +packageFileInfo['tmp_name']); + $zipContent = $zipFile->listContent(); + + $packageSize = array_reduce( + $zipContent, + function ($accumulator, $zipEntry) { + if (preg_match('~.(php.*|phtml)$~i', $zipEntry['filename'])) { + throw new Exception("File \"{$zipEntry['filename']}\" contains a PHP script"); + } + + if (in_array($zipEntry['filename'], ['tincan.xml', 'cmi5.xml'])) { + $this->packageType = explode('.', $zipEntry['filename'], 2)[0]; + } + + return $accumulator + $zipEntry['size']; + } + ); + + if (empty($this->packageType)) { + throw new Exception('Invalid package'); + } + + $this->validateEnoughSpace($packageSize); + + $pathInfo = pathinfo($this->packageFileInfo['name']); + + $packageDirectoryPath = $this->generatePackageDirectory($pathInfo['filename']); + + $zipFile->extract($packageDirectoryPath); + + return "$packageDirectoryPath/{$this->packageType}.xml"; + } + + /** + * @param int $packageSize + * + * @throws \Exception + */ + protected function validateEnoughSpace(int $packageSize) + { + $courseSpaceQuota = DocumentManager::get_course_quota($this->course->getCode()); + + if (!enough_size($packageSize, $this->courseDirectoryPath, $courseSpaceQuota)) { + throw new Exception('Not enough space to storage package.'); + } + } + + /** + * @param string $name + * + * @return string + */ + private function generatePackageDirectory(string $name): string + { + $directoryPath = implode( + '/', + [ + $this->courseDirectoryPath, + $this->packageType, + api_replace_dangerous_char($name), + ] + ); + + $fs = new Filesystem(); + $fs->mkdir( + $directoryPath, + api_get_permissions_for_new_directories() + ); + + return $directoryPath; + } +} diff --git a/plugin/xapi/src/Parser/AbstractParser.php b/plugin/xapi/src/Parser/AbstractParser.php deleted file mode 100644 index 933585131b..0000000000 --- a/plugin/xapi/src/Parser/AbstractParser.php +++ /dev/null @@ -1,53 +0,0 @@ -filePath = $filePath; - $this->course = $course; - $this->session = $session; - } - - /** - * @param string $filePath - * - * @return mixed - */ - abstract public static function create($filePath, Course $course, Session $session = null); - - /** - * @return \Chamilo\PluginBundle\Entity\XApi\ToolLaunch - */ - abstract public function parse(); -} diff --git a/plugin/xapi/src/Parser/Cmi5Parser.php b/plugin/xapi/src/Parser/Cmi5Parser.php index ecbe95ac6c..2897cb42cd 100644 --- a/plugin/xapi/src/Parser/Cmi5Parser.php +++ b/plugin/xapi/src/Parser/Cmi5Parser.php @@ -4,8 +4,6 @@ namespace Chamilo\PluginBundle\XApi\Parser; -use Chamilo\CoreBundle\Entity\Course; -use Chamilo\CoreBundle\Entity\Session; use Chamilo\PluginBundle\Entity\XApi\Cmi5Item; use Chamilo\PluginBundle\Entity\XApi\ToolLaunch; use Symfony\Component\DomCrawler\Crawler; @@ -15,25 +13,12 @@ use Symfony\Component\DomCrawler\Crawler; * * @package Chamilo\PluginBundle\XApi\Parser */ -class Cmi5Parser extends AbstractParser +class Cmi5Parser extends PackageParser { /** - * @var array|\Chamilo\PluginBundle\Entity\XApi\Cmi5Item[] + * @inheritDoc */ - private $toc; - - /** - * {@inheritdoc} - */ - public static function create($filePath, Course $course, Session $session = null) - { - return new self($filePath, $course, $session); - } - - /** - * {@inheritdoc} - */ - public function parse() + public function parse(): ToolLaunch { $content = file_get_contents($this->filePath); $xml = new Crawler($content); @@ -64,17 +49,13 @@ class Cmi5Parser extends AbstractParser ->setCourse($this->course) ->setSession($this->session); - $this->toc = $this->generateToC($xml); + $toc = $this->generateToC($xml); - return $toolLaunch; - } + foreach ($toc as $cmi5Item) { + $toolLaunch->addItem($cmi5Item); + } - /** - * @return array|\Chamilo\PluginBundle\Entity\XApi\Cmi5Item[] - */ - public function getToc() - { - return $this->toc; + return $toolLaunch; } /** @@ -133,9 +114,8 @@ class Cmi5Parser extends AbstractParser if ('au' === $node->nodeName()) { $launchParametersNode = $node->filterXPath('//launchParameters'); $entitlementKeyNode = $node->filterXPath('//entitlementKey'); - $url = - - $item + $url + = $item ->setUrl( $this->parseLaunchUrl( trim($node->filterXPath('//url')->text()) diff --git a/plugin/xapi/src/Parser/PackageParser.php b/plugin/xapi/src/Parser/PackageParser.php new file mode 100644 index 0000000000..a1c1f67bbd --- /dev/null +++ b/plugin/xapi/src/Parser/PackageParser.php @@ -0,0 +1,70 @@ +filePath = $filePath; + $this->course = $course; + $this->session = $session; + } + + /** + * @param string $packageType + * @param string $filePath + * @param \Chamilo\CoreBundle\Entity\Course $course + * @param \Chamilo\CoreBundle\Entity\Session|null $session + * + * @throws \Exception + * + * @return mixed + */ + public static function create(string $packageType, string $filePath, Course $course, Session $session = null) + { + switch ($packageType) { + case 'tincan': + return new TinCanParser($filePath, $course, $session); + case 'cmi5': + return new Cmi5Parser($filePath, $course, $session); + default: + throw new \Exception('Invalid package.'); + } + } + + /** + * @return \Chamilo\PluginBundle\Entity\XApi\ToolLaunch + */ + abstract public function parse(): \Chamilo\PluginBundle\Entity\XApi\ToolLaunch; +} diff --git a/plugin/xapi/src/Parser/TinCanParser.php b/plugin/xapi/src/Parser/TinCanParser.php index 7a8531cceb..13c62b1198 100644 --- a/plugin/xapi/src/Parser/TinCanParser.php +++ b/plugin/xapi/src/Parser/TinCanParser.php @@ -4,8 +4,6 @@ namespace Chamilo\PluginBundle\XApi\Parser; -use Chamilo\CoreBundle\Entity\Course; -use Chamilo\CoreBundle\Entity\Session; use Chamilo\PluginBundle\Entity\XApi\ToolLaunch; use Symfony\Component\DomCrawler\Crawler; @@ -14,20 +12,12 @@ use Symfony\Component\DomCrawler\Crawler; * * @package Chamilo\PluginBundle\XApi\Parser */ -class TinCanParser extends AbstractParser +class TinCanParser extends PackageParser { /** - * {@inheritdoc} + * @inheritDoc */ - public static function create($filePath, Course $course, Session $session = null) - { - return new self($filePath, $course, $session); - } - - /** - * {@inheritdoc} - */ - public function parse() + public function parse(): ToolLaunch { $content = file_get_contents($this->filePath); @@ -61,7 +51,7 @@ class TinCanParser extends AbstractParser /** * @return string */ - private function parseLaunchUrl(Crawler $launchNode) + private function parseLaunchUrl(Crawler $launchNode): string { $launchUrl = $launchNode->text(); @@ -79,4 +69,5 @@ class TinCanParser extends AbstractParser return $launchUrl; } + } diff --git a/plugin/xapi/src/XApiPlugin.php b/plugin/xapi/src/XApiPlugin.php index 6db7b9c9b6..dfb3493208 100644 --- a/plugin/xapi/src/XApiPlugin.php +++ b/plugin/xapi/src/XApiPlugin.php @@ -317,7 +317,7 @@ class XApiPlugin extends Plugin implements HookPluginInterface $this->get_lang('ToolTinCan'), $courseId, null, - 'xapi/tincan/index.php' + 'xapi/start.php' ); } @@ -525,10 +525,6 @@ class XApiPlugin extends Plugin implements HookPluginInterface { Database::getManager() ->createQuery('DELETE FROM ChamiloCourseBundle:CTool t WHERE t.category = :category AND t.link LIKE :link') - ->execute(['category' => 'plugin', 'link' => 'xapi/tincan/index.php%']); - - Database::getManager() - ->createQuery('DELETE FROM ChamiloCourseBundle:CTool t WHERE t.category = :category AND t.link LIKE :link') - ->execute(['category' => 'plugin', 'link' => 'xapi/cmi5/index.php%']); + ->execute(['category' => 'plugin', 'link' => 'xapi/start.php%']); } } diff --git a/plugin/xapi/tincan/index.php b/plugin/xapi/start.php similarity index 82% rename from plugin/xapi/tincan/index.php rename to plugin/xapi/start.php index 6b422d52bd..e30f6fd6d3 100644 --- a/plugin/xapi/tincan/index.php +++ b/plugin/xapi/start.php @@ -4,7 +4,7 @@ use Chamilo\PluginBundle\Entity\XApi\ToolLaunch; -require_once __DIR__.'/../../../main/inc/global.inc.php'; +require_once __DIR__.'/../../main/inc/global.inc.php'; api_protect_course_script(true); api_block_anonymous_users(); @@ -68,7 +68,7 @@ $table->set_column_filter( $data = Display::url( $title, - ('cmi5' === $ativityType ? '../cmi5/tool.php' : 'tool.php')."?id=$id&$cidReq", + ('cmi5' === $ativityType ? 'cmi5/view.php' : 'tincan/view.php')."?id=$id&$cidReq", ['class' => 'show'] ); @@ -81,7 +81,9 @@ $table->set_column_filter( ); if ($isAllowedToEdit) { - $table->set_header(1, get_lang('Actions'), false, ['class' => 'text-right'], ['class' => 'text-right']); + $thAttributes = ['class' => 'text-right', 'style' => 'width: 100px;']; + + $table->set_header(1, get_lang('Actions'), false, $thAttributes, $thAttributes); $table->set_column_filter( 1, function ($id) use ($cidReq, $isAllowedToEdit) { @@ -90,15 +92,15 @@ if ($isAllowedToEdit) { if ($isAllowedToEdit) { $actions[] = Display::url( Display::return_icon('statistics.png', get_lang('Reporting')), - "stats.php?$cidReq&id=$id" + "tincan/stats.php?$cidReq&id=$id" ); $actions[] = Display::url( Display::return_icon('edit.png', get_lang('Edit')), - "edit.php?$cidReq&edit=$id" + "tool_edit.php?$cidReq&edit=$id" ); $actions[] = Display::url( Display::return_icon('delete.png', get_lang('Delete')), - "delete.php?$cidReq&delete=$id" + "tool_delete.php?$cidReq&delete=$id" ); } @@ -119,14 +121,9 @@ $view->assign('header', $pageTitle); if ($isAllowedToEdit) { $actions = Display::url( - Display::return_icon('add.png', get_lang('Add'), [], ICON_SIZE_MEDIUM), - "add.php?$cidReq" - ) - .PHP_EOL - .Display::url( - Display::return_icon('add.png', get_lang('Add'), [], ICON_SIZE_MEDIUM), - "../cmi5/add.php?$cidReq" - ); + Display::return_icon('import_scorm.png', get_lang('Import'), [], ICON_SIZE_MEDIUM), + "tool_import.php?$cidReq" + ); $view->assign( 'actions', diff --git a/plugin/xapi/tincan/stats.php b/plugin/xapi/tincan/stats.php index 46e8875c58..b019db5d91 100644 --- a/plugin/xapi/tincan/stats.php +++ b/plugin/xapi/tincan/stats.php @@ -109,7 +109,7 @@ $content .= $pagination; // View $interbreadcrumb[] = [ 'name' => $plugin->get_title(), - 'url' => 'index.php', + 'url' => '../start.php', ]; $htmlHeadXtra[] = "