diff --git a/Makefile b/Makefile index 18a43d1e2..34020fbe5 100644 --- a/Makefile +++ b/Makefile @@ -298,6 +298,24 @@ export-elp-epub3: export-elp-ims: @$(MAKE) export-elp FORMAT=ims INPUT="$(INPUT)" OUTPUT="$(OUTPUT)" DEBUG="$(DEBUG)" BASE_URL="$(BASE_URL)" +export-elp-h5p: +ifndef INPUT + $(error INPUT is required. Use INPUT=/absolute/path/to/file.elp) +endif +ifndef OUTPUT + $(error OUTPUT is required. Use OUTPUT=/absolute/path/to/output.h5p) +endif + $(eval EXPANDED_INPUT := $(call EXPAND_PATH,$(INPUT))) + @if [ ! -f "$(EXPANDED_INPUT)" ]; then \ + echo "❌ INPUT file does not exist: $(EXPANDED_INPUT)"; \ + exit 1; \ + fi + @mkdir -p $(dir $(OUTPUT)) + $(eval TMP_DIR := $(OUTPUT).tmpdir) + @$(MAKE) export-elp FORMAT=h5p INPUT="$(EXPANDED_INPUT)" OUTPUT="$(TMP_DIR)" DEBUG="$(DEBUG)" BASE_URL="$(BASE_URL)" + @mv "$(TMP_DIR)"/*.h5p "$(OUTPUT)" + @rm -rf "$(TMP_DIR)" + # Install nativephp/php-bin package temporarily without modifying composer.json install-php-bin: @@ -388,6 +406,7 @@ help: @echo " export-elp-scorm2004 - Export .elp to SCORM 2004 format (alias for FORMAT=scorm2004)" @echo " export-elp-ims - Export .elp to IMS format (alias for FORMAT=ims)" @echo " export-elp-epub3 - Export .elp to EPUB 3 format (alias for FORMAT=epub3)" + @echo " export-elp-h5p - Export .elp to H5P (experimental) format (alias for FORMAT=h5p)" @echo " export-elp-elp - Re-export .elp file (alias for FORMAT=elp)" @echo "" @echo "Data:" diff --git a/README.md b/README.md index f98c5be82..fba9bd640 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ This version is built with modern technologies (PHP 8, Symfony 7) and provides a * Creation and edition of interactive educational content * Multiple iDevices (interactive elements) * Multilingual support -* Exportation to various formats +* Exportation to multiple formats (SCORM, IMS, EPUB, HTML and H5P) * Moodle integration * Real-time collaborative features powered by [Mercure](https://mercure.rocks/) * Modern and accessible interface built with [Bootstrap](https://getbootstrap.com/) diff --git a/composer.json b/composer.json index dfcb0951f..ab556b47c 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,7 @@ "doctrine/doctrine-migrations-bundle": "^3.0", "doctrine/orm": "^3.0", "firebase/php-jwt": "^6.10", + "h5p/h5p-core": "^1.27", "league/flysystem": "^1.0", "league/flysystem-ziparchive": "^1.0", "liip/imagine-bundle": "^2.8", diff --git a/composer.lock b/composer.lock index 654e44522..48b758bbf 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ac1985069fa92d52cb0f1a3e1ad57c5a", + "content-hash": "d4679a1ca5108646dc6db741f091b937", "packages": [ { "name": "doctrine/collections", @@ -1255,6 +1255,67 @@ }, "time": "2025-04-09T20:32:01+00:00" }, + { + "name": "h5p/h5p-core", + "version": "1.27.0", + "source": { + "type": "git", + "url": "https://github.com/h5p/h5p-php-library.git", + "reference": "829524eaf81fe3f3a295d0e843812be4735f51fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/h5p/h5p-php-library/zipball/829524eaf81fe3f3a295d0e843812be4735f51fc", + "reference": "829524eaf81fe3f3a295d0e843812be4735f51fc", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "type": "library", + "autoload": { + "files": [ + "h5p.classes.php", + "h5p-development.class.php", + "h5p-file-storage.interface.php", + "h5p-default-storage.class.php", + "h5p-event-base.class.php", + "h5p-metadata.class.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0" + ], + "authors": [ + { + "name": "Svein-Tore Griff With", + "email": "with@joubel.com", + "homepage": "http://joubel.com", + "role": "CEO" + }, + { + "name": "Frode Petterson", + "email": "frode.petterson@joubel.com", + "homepage": "http://joubel.com", + "role": "Developer" + } + ], + "description": "H5P Core functionality in PHP", + "homepage": "https://h5p.org", + "keywords": [ + "content", + "h5p", + "hvp", + "interactive", + "quiz" + ], + "support": { + "issues": "https://github.com/h5p/h5p-php-library/issues", + "source": "https://github.com/h5p/h5p-php-library/tree/1.27.0" + }, + "time": "2024-10-25T11:30:55+00:00" + }, { "name": "imagine/imagine", "version": "1.5.0", @@ -11274,13 +11335,13 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { "php": ">=8.1", "ext-ctype": "*" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/doc/03-development-environment.md b/doc/03-development-environment.md index 993ed3989..3e738990f 100644 --- a/doc/03-development-environment.md +++ b/doc/03-development-environment.md @@ -136,10 +136,20 @@ The project provides a Makefile to simplify common tasks: | `make test` | Run unit tests | | `make lint` | Check PHP code style | | `make fix` | Automatically fix code style issues | -| `make update` | Update Composer dependencies | -| `make translations` | Update translation files | - -### Installing `make` +| `make update` | Update Composer dependencies | +| `make translations` | Update translation files | + +### Exporting Content + +Use the Makefile to generate packages from `.elp` projects. For example, to create an experimental H5P package: + +```bash +make export-elp-h5p INPUT=/path/project.elp OUTPUT=/path/project.h5p +``` + +Other formats can be exported by replacing `h5p` with `html5`, `scorm12`, `scorm2004`, `ims`, or `epub3`. + +### Installing `make` On Linux, `make` is usually pre-installed. On Windows, install it with: diff --git a/public/app/workarea/menus/navbar/items/navbarFile.js b/public/app/workarea/menus/navbar/items/navbarFile.js index 07be5116c..9331245fc 100644 --- a/public/app/workarea/menus/navbar/items/navbarFile.js +++ b/public/app/workarea/menus/navbar/items/navbarFile.js @@ -46,6 +46,9 @@ export default class NavbarFile { this.exportEPUB3Button = this.menu.navbar.querySelector( '#navbar-button-export-epub3', ); + this.exportH5PButton = this.menu.navbar.querySelector( + '#navbar-button-export-h5p', + ); this.exportXmlPropertiesButton = this.menu.navbar.querySelector( '#navbar-button-export-xml-properties', ); @@ -79,6 +82,7 @@ export default class NavbarFile { this.setExportSCORM2004Event(); this.setExportIMSEvent(); this.setExportEPUB3Event(); + this.setExportH5PEvent(); this.setExportXmlPropertiesEvent(); this.setImportXmlPropertiesEvent(); this.setLeftPanelsTogglerEvents(); @@ -274,6 +278,13 @@ export default class NavbarFile { }); } + setExportH5PEvent() { + this.exportH5PButton.addEventListener('click', () => { + if (eXeLearning.app.project.checkOpenIdevice()) return; + this.exportH5PEvent(); + }); + } + /** * Download the project to ePub3 * File -> Export as -> Xml properties @@ -1086,6 +1097,41 @@ export default class NavbarFile { eXeLearning.app.interface.connectionTime.loadLasUpdatedInInterface(); } + async exportH5PEvent() { + let toastData = { + title: _('Export'), + body: _('Generating export files...'), + icon: 'downloading', + }; + let toast = eXeLearning.app.toasts.createToast(toastData); + let odeSessionId = eXeLearning.app.project.odeSession; + let response = await eXeLearning.app.api.getOdeExportDownload( + odeSessionId, + 'h5p', + ); + if (response['responseMessage'] == 'OK') { + this.downloadLink( + response['urlZipFile'], + response['exportProjectName'], + ); + toast.toastBody.innerHTML = _('The project has been exported.'); + } else { + toast.toastBody.innerHTML = _('An error occurred while exporting the project.'); + toast.toastBody.classList.add('error'); + eXeLearning.app.modals.alert.show({ + title: _('Error'), + body: response['responseMessage'] ? response['responseMessage'] : _('Unknown error.'), + contentId: 'error', + }); + } + + setTimeout(() => { + toast.remove(); + }, 1000); + + eXeLearning.app.interface.connectionTime.loadLasUpdatedInInterface(); + } + /** * Export the properties as xml and download it * diff --git a/src/Command/net/exelearning/Command/ElpExportCommand.php b/src/Command/net/exelearning/Command/ElpExportCommand.php index a40c1ce98..de9732c74 100644 --- a/src/Command/net/exelearning/Command/ElpExportCommand.php +++ b/src/Command/net/exelearning/Command/ElpExportCommand.php @@ -41,7 +41,7 @@ protected function configure(): void $this ->addArgument('input', InputArgument::REQUIRED, 'Input ELP file path (use "-" for stdin)') ->addArgument('output', InputArgument::REQUIRED, 'Output directory') - ->addArgument('format', InputArgument::OPTIONAL, 'Export format (elp, html5, html5-sp, scorm12, scorm2004, ims, epub3)', $this->defaultFormat) + ->addArgument('format', InputArgument::OPTIONAL, 'Export format (elp, html5, html5-sp, scorm12, scorm2004, ims, epub3, h5p)', $this->defaultFormat) ->addOption('debug', 'd', InputOption::VALUE_NONE, 'Enable debug mode') ->addOption('base-url', 'b', InputOption::VALUE_OPTIONAL, 'Base URL for links', false); } @@ -63,6 +63,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int Constants::EXPORT_TYPE_SCORM2004, Constants::EXPORT_TYPE_IMS, Constants::EXPORT_TYPE_EPUB3, + Constants::EXPORT_TYPE_H5P, ]; if (!in_array($format, $validFormats, true)) { diff --git a/src/Command/net/exelearning/Command/ElpExportH5pCommand.php b/src/Command/net/exelearning/Command/ElpExportH5pCommand.php new file mode 100644 index 000000000..abeb0468e --- /dev/null +++ b/src/Command/net/exelearning/Command/ElpExportH5pCommand.php @@ -0,0 +1,15 @@ +entityManager = $entityManager; @@ -86,6 +89,7 @@ public function __construct( $this->exportSCORM2004Service = $exportSCORM2004Service; $this->exportIMSService = $exportIMSService; $this->exportEPUB3Service = $exportEPUB3Service; + $this->exportH5PService = $exportH5PService; $this->slugger = $slugger; } @@ -306,6 +310,10 @@ public function export( $ext = Constants::FILE_EXTENSION_ZIP; $typeSuffix = Constants::SUFFIX_TYPE_IMS; break; + case Constants::EXPORT_TYPE_H5P: + $ext = Constants::FILE_EXTENSION_H5P; + $typeSuffix = Constants::SUFFIX_TYPE_H5P; + break; default: $ext = Constants::FILE_EXTENSION_ZIP; break; @@ -568,79 +576,89 @@ public function generateExportStructure( // COPY FILES // /////////////////////////////////////////////////// - // TODO VER QUé HACE PORQUE NO COPIA NADA en export ELP **************** - // Copy project schema files - $this->copySchemaFilesToExportDir($exportDirPath, $exportType); - - // Export type content directory - // In some exports the content is placed in a subdirectory $newExportDirPath = $exportDirPath; - if (isset(Constants::EXPORT_DIR_CONTENT_BY_EXPORT[$exportType])) { - $newExportDirName = Constants::EXPORT_DIR_CONTENT_BY_EXPORT[$exportType]; - $newExportDirPath = self::createDirInExportDir($exportDirPath, $newExportDirName); + if (Constants::EXPORT_TYPE_H5P !== $exportType) { + // TODO VER QUé HACE PORQUE NO COPIA NADA en export ELP **************** + // Copy project schema files + $this->copySchemaFilesToExportDir($exportDirPath, $exportType); + + // Export type content directory + // In some exports the content is placed in a subdirectory + if (isset(Constants::EXPORT_DIR_CONTENT_BY_EXPORT[$exportType])) { + $newExportDirName = Constants::EXPORT_DIR_CONTENT_BY_EXPORT[$exportType]; + $newExportDirPath = self::createDirInExportDir($exportDirPath, $newExportDirName); + } + // TODO VER QUé HACE PORQUE NO COPIA NADA en export ELP **************** + // Copy project common files + $this->copyCommonFilesToExportDir($newExportDirPath, $exportType); } - // TODO VER QUé HACE PORQUE NO COPIA NADA en export ELP **************** - // Copy project common files - $this->copyCommonFilesToExportDir($newExportDirPath, $exportType); // vamos a crear un método que analice recursivamente odeNavStructureSyncs y que copie los archivos de las librerías que se deben de incluir en el proyecto // debe de buscar en cada idevice si tiene una algún efecto incluido. El método retornará todos un array con todos los efectos que encuentre. - list($librariesToCopy, $librariesFileToCopy) = ExportXmlUtil::getPathForLibrariesInIdevices($odeNavStructureSyncs, $odeProperties); - - // copia todos los libs, jquery, bootstrap - // Copy project base files to export --> Aquí vamos a copiar los obligatorios - $this->copyBaseFilesToExportDir($newExportDirPath, $exportType, $resourcesPrefix, $isPreview, $librariesToCopy); - - // Copy ode files + $libsResourcesPath = []; $idevicesMapping = $odeSaveXML->getOdeComponentsMapping(); - $idevicesMapping['odeFileNames'] = $this->odeService->copyOdeFilesToDist( - $odeSessionId, - $idevicesMapping, - $dbUser, - $newExportDirPath - ); + $idevicesTypesData = []; + if (Constants::EXPORT_TYPE_H5P !== $exportType) { + list($librariesToCopy, $librariesFileToCopy) = ExportXmlUtil::getPathForLibrariesInIdevices($odeNavStructureSyncs, $odeProperties); - // Modify the urls of the base style in the preview - $baseCssPath = $newExportDirPath.Constants::PERMANENT_SAVE_CONTENT_DIRNAME.DIRECTORY_SEPARATOR. - Constants::PERMANENT_SAVE_CONTENT_CSS_DIRNAME.DIRECTORY_SEPARATOR. - Constants::WORKAREA_STYLE_BASE_CSS_FILENAME; - $this->replaceUrlsBaseCssFile($baseCssPath, $resourcesPrefix, $isPreview); - // array con todos las rutas de las librerías - // Get links to files (previously copied files) - $libsResourcesPath = $this->getFilesLoadedPath($exportType, $librariesFileToCopy); - - // Copy idevices export files - $idevicesTypesData = $this->copyIdevicesToExportDir( - $user, - $odeNavStructureSyncs, - $newExportDirPath, - $resourcesPrefix, - $isPreview, - $exportType - ); + // copia todos los libs, jquery, bootstrap + // Copy project base files to export --> Aquí vamos a copiar los obligatorios + $this->copyBaseFilesToExportDir($newExportDirPath, $exportType, $resourcesPrefix, $isPreview, $librariesToCopy); - // Copy ELP to the export dir if it is necessary - $elpFileName = false; - if ($addElpToExport || ('true' == $odeProperties['pp_exportElp']->getValue())) { - $elpFileName = $this->copyDistElpToExport( - $dbUser, - $odeId, + // Copy ode files + $idevicesMapping['odeFileNames'] = $this->odeService->copyOdeFilesToDist( $odeSessionId, - $odeVersionId, - $odeProperties, - $saveOdeResultParameters['elpFileName'], + $idevicesMapping, + $dbUser, $newExportDirPath ); + + // Modify the urls of the base style in the preview + $baseCssPath = $newExportDirPath.Constants::PERMANENT_SAVE_CONTENT_DIRNAME.DIRECTORY_SEPARATOR. + Constants::PERMANENT_SAVE_CONTENT_CSS_DIRNAME.DIRECTORY_SEPARATOR. + Constants::WORKAREA_STYLE_BASE_CSS_FILENAME; + $this->replaceUrlsBaseCssFile($baseCssPath, $resourcesPrefix, $isPreview); + + // array con todos las rutas de las librerías + // Get links to files (previously copied files) + $libsResourcesPath = $this->getFilesLoadedPath($exportType, $librariesFileToCopy); + + // Copy idevices export files + $idevicesTypesData = $this->copyIdevicesToExportDir( + $user, + $odeNavStructureSyncs, + $newExportDirPath, + $resourcesPrefix, + $isPreview, + $exportType + ); } - // Copy theme files - $themeServerDir = $this->themeHelper->getThemeDir( - $theme->getDirName(), - $theme->getType(), - $dbUser - ); - $exportDirThemePath = $newExportDirPath.Constants::EXPORT_DIR_THEME; - FileUtil::copyDir($themeServerDir, $exportDirThemePath); + + // Copy ELP to the export dir if it is necessary + $elpFileName = false; + if (Constants::EXPORT_TYPE_H5P !== $exportType) { + if ($addElpToExport || ('true' == $odeProperties['pp_exportElp']->getValue())) { + $elpFileName = $this->copyDistElpToExport( + $dbUser, + $odeId, + $odeSessionId, + $odeVersionId, + $odeProperties, + $saveOdeResultParameters['elpFileName'], + $newExportDirPath + ); + } + + // Copy theme files + $themeServerDir = $this->themeHelper->getThemeDir( + $theme->getDirName(), + $theme->getType(), + $dbUser + ); + $exportDirThemePath = $newExportDirPath.Constants::EXPORT_DIR_THEME; + FileUtil::copyDir($themeServerDir, $exportDirThemePath); + } // /////////////////////////////////////////////////// @@ -658,40 +676,41 @@ public function generateExportStructure( // Clone idevices $odeComponentsSyncCloneArray = []; + if (Constants::EXPORT_TYPE_H5P !== $exportType) { + // Replace urls in idevice + foreach ($odeNavStructureSyncs as $odeNavStructureSync) { + foreach ($odeNavStructureSync->getOdePagStructureSyncs() as $odePagStructureSync) { + foreach ($odePagStructureSync->getOdeComponentsSyncs() as $odeComponentsSync) { + $ideviceId = $odeComponentsSync->getOdeIdeviceId(); + $newIdeviceId = $idevicesMapping[$ideviceId]; + $ideviceResourcesMapping = $idevicesMapping['odeFileNames']['odeComponents'][$newIdeviceId]; + $filemanagerResourcesMapping = $idevicesMapping['odeFileNames']['fileManager']; - // Replace urls in idevice - foreach ($odeNavStructureSyncs as $odeNavStructureSync) { - foreach ($odeNavStructureSync->getOdePagStructureSyncs() as $odePagStructureSync) { - foreach ($odePagStructureSync->getOdeComponentsSyncs() as $odeComponentsSync) { - $ideviceId = $odeComponentsSync->getOdeIdeviceId(); - $newIdeviceId = $idevicesMapping[$ideviceId]; - $ideviceResourcesMapping = $idevicesMapping['odeFileNames']['odeComponents'][$newIdeviceId]; - $filemanagerResourcesMapping = $idevicesMapping['odeFileNames']['fileManager']; + $odeComponentsSyncClone = clone $odeComponentsSync; - $odeComponentsSyncClone = clone $odeComponentsSync; + // Page file path/url + $pageData = $pagesFileData[$odeNavStructureSync->getOdePageId()]; - // Page file path/url - $pageData = $pagesFileData[$odeNavStructureSync->getOdePageId()]; + // In case it is not a preview we need to adjust the url of the links since we will be in a subfolder + if (!$pageData['isIndex'] && !$isPreview && Constants::EXPORT_TYPE_HTML5_SP != $exportType) { + $newResourcesPrefix = '..'.Constants::SLASH.$resourcesPrefix; + } else { + $newResourcesPrefix = $resourcesPrefix; + } - // In case it is not a preview we need to adjust the url of the links since we will be in a subfolder - if (!$pageData['isIndex'] && !$isPreview && Constants::EXPORT_TYPE_HTML5_SP != $exportType) { - $newResourcesPrefix = '..'.Constants::SLASH.$resourcesPrefix; - } else { - $newResourcesPrefix = $resourcesPrefix; + $odeComponentsSyncClone->replaceLinksHtml( + $newIdeviceId, + $ideviceResourcesMapping, + $filemanagerResourcesMapping, + $pagesFileData, + $userPreferencesDtos, + $elpFileName, + $newResourcesPrefix, + $exportType + ); + + $odeComponentsSyncCloneArray[$ideviceId] = $odeComponentsSyncClone; } - - $odeComponentsSyncClone->replaceLinksHtml( - $newIdeviceId, - $ideviceResourcesMapping, - $filemanagerResourcesMapping, - $pagesFileData, - $userPreferencesDtos, - $elpFileName, - $newResourcesPrefix, - $exportType - ); - - $odeComponentsSyncCloneArray[$ideviceId] = $odeComponentsSyncClone; } } } @@ -701,12 +720,13 @@ public function generateExportStructure( // /////////////////////////////////////////////////// // Create the config.xml in case the export is a ELP or editable export preference is activated - if (Constants::EXPORT_TYPE_ELP == $exportType + if (Constants::EXPORT_TYPE_H5P !== $exportType && ( + Constants::EXPORT_TYPE_ELP == $exportType || ((('true' == $odeProperties['exportSource']->getValue()) || $isIntegration) && (Constants::EXPORT_TYPE_HTML5 == $exportType || Constants::EXPORT_TYPE_SCORM12 == $exportType || Constants::EXPORT_TYPE_SCORM2004 == $exportType || Constants::EXPORT_TYPE_HTML5_SP == $exportType - || Constants::EXPORT_TYPE_IMS == $exportType) - ) + || Constants::EXPORT_TYPE_IMS == $exportType)) + ) ) { $configFileName = Constants::PERMANENT_SAVE_CONTENT_FILENAME; $xmlFilePathName = $newExportDirPath.$configFileName; @@ -797,8 +817,10 @@ private function generateExportContentByExportType( $exportType ); - // Create export pages dir - self::createPagesDir($exportDirPath); + // Create export pages dir except for H5P + if (Constants::EXPORT_TYPE_H5P !== $exportType) { + self::createPagesDir($exportDirPath); + } // Get service by export type switch ($exportType) { @@ -814,6 +836,9 @@ private function generateExportContentByExportType( case Constants::EXPORT_TYPE_EPUB3: $exportService = $this->exportEPUB3Service; break; + case Constants::EXPORT_TYPE_H5P: + $exportService = $this->exportH5PService; + break; case Constants::EXPORT_TYPE_HTML5_SP: $exportService = $this->exportHTML5SPService; break; @@ -1214,7 +1239,8 @@ private static function createZipOdeExportFilesToDist( $name = self::generateExportFileName($odeId, $odeVersionId, null, $exportType, $slug, $typeSuffix); $zipPath = $exportDirPath.$name; - FileUtil::zipDir($exportDirPath, $zipPath); + $includeDirs = Constants::FILE_EXTENSION_H5P !== $exportType; + FileUtil::zipDir($exportDirPath, $zipPath, $includeDirs); return $name; } diff --git a/src/Service/net/exelearning/Service/Export/ExportH5PService.php b/src/Service/net/exelearning/Service/Export/ExportH5PService.php new file mode 100644 index 000000000..35ad77303 --- /dev/null +++ b/src/Service/net/exelearning/Service/Export/ExportH5PService.php @@ -0,0 +1,151 @@ +exportType = Constants::EXPORT_TYPE_H5P; + $this->fileHelper = $fileHelper; + $this->currentOdeUsersService = $currentOdeUsersService; + $this->translator = $translator; + } + + public function generateExportFiles( + $user, + $odeSessionId, + $odeNavStructureSyncs, + $pagesFileData, + $odeProperties, + $libsResourcesPath, + $odeComponentsSyncCloneArray, + $idevicesMapping, + $idevicesByPage, + $idevicesTypesData, + $userPreferencesDtos, + $theme, + $elpFileName, + $resourcesPrefix, + $isPreview, + $translator, + ) { + $exportDirPath = $this->fileHelper->getOdeSessionUserTmpExportDir($odeSessionId, $user); + $contentDir = $exportDirPath.'content'.DIRECTORY_SEPARATOR; + if (!is_dir($contentDir)) { + mkdir($contentDir, 0775, true); + } + + $projectTitle = isset($odeProperties['pp_title']) + ? trim(strip_tags($odeProperties['pp_title']->getValue())) + : 'eXeLearning project'; + $language = isset($odeProperties['pp_lang']) + ? substr($odeProperties['pp_lang']->getValue(), 0, 2) + : 'en'; + + $html = ''; + foreach ($odeNavStructureSyncs as $page) { + $pageId = $page->getOdePageId(); + $pageData = $pagesFileData[$pageId]; + $html .= '