From 1547b66ec9a405a8c94e74b74f0f1730052ee40d Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 16 Nov 2021 11:08:51 +0100 Subject: [PATCH] MOBILE-3917 h5p: Check missing dependencies --- scripts/lang_functions.php | 1 + scripts/langindex.json | 1 + src/core/features/h5p/classes/core.ts | 7 ++ src/core/features/h5p/classes/framework.ts | 15 +++- src/core/features/h5p/classes/helper.ts | 2 +- src/core/features/h5p/classes/storage.ts | 16 ++-- src/core/features/h5p/classes/validator.ts | 91 +++++++++++++++++++++- src/core/features/h5p/lang.json | 3 +- src/core/features/h5p/services/h5p.ts | 2 +- 9 files changed, 123 insertions(+), 15 deletions(-) diff --git a/scripts/lang_functions.php b/scripts/lang_functions.php index a0cd6f9ba..cd363ee48 100644 --- a/scripts/lang_functions.php +++ b/scripts/lang_functions.php @@ -246,6 +246,7 @@ function build_lang($lang, $keys) { } if ($value->file != 'local_moodlemobileapp') { + $text = str_replace('$a->@', '$a.', $text); $text = str_replace('$a->', '$a.', $text); $text = str_replace('{$a', '{{$a', $text); $text = str_replace('}', '}}', $text); diff --git a/scripts/langindex.json b/scripts/langindex.json index 1df9a198e..3fbdd7b43 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1749,6 +1749,7 @@ "core.h5p.licensee": "h5p", "core.h5p.licenseextras": "h5p", "core.h5p.licenseversion": "h5p", + "core.h5p.missingdependency": "h5p", "core.h5p.nocopyright": "h5p", "core.h5p.offlineDialogBody": "h5p", "core.h5p.offlineDialogHeader": "h5p", diff --git a/src/core/features/h5p/classes/core.ts b/src/core/features/h5p/classes/core.ts index 1e6a90502..6ba4aeec8 100644 --- a/src/core/features/h5p/classes/core.ts +++ b/src/core/features/h5p/classes/core.ts @@ -987,6 +987,13 @@ export type CoreH5PLibraryBasicData = { minorVersion: number; // Minor version. }; +/** + * Data about a missing library. + */ +export type CoreH5PMissingLibrary = CoreH5PLibraryBasicData & { + libString: string; // Library that has the dependency. +}; + /** * Library basic data including patch version. */ diff --git a/src/core/features/h5p/classes/framework.ts b/src/core/features/h5p/classes/framework.ts index 326d7dc05..cfb77cac5 100644 --- a/src/core/features/h5p/classes/framework.ts +++ b/src/core/features/h5p/classes/framework.ts @@ -42,6 +42,7 @@ import { CoreH5PSemantics } from './content-validator'; import { CoreH5PContentBeingSaved, CoreH5PLibraryBeingSaved } from './storage'; import { CoreH5PLibraryAddTo, CoreH5PLibraryMetadataSettings } from './validator'; import { CoreH5PMetadata } from './metadata'; +import { Translate } from '@singletons'; /** * Equivalent to Moodle's implementation of H5PFrameworkInterface. @@ -742,14 +743,14 @@ export class CoreH5PFramework { /** * Save what libraries a library is depending on. * - * @param libraryId Library Id for the library we're saving dependencies for. + * @param library Library data for the library we're saving dependencies for. * @param dependencies List of dependencies as associative arrays containing machineName, majorVersion, minorVersion. * @param dependencytype The type of dependency. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ async saveLibraryDependencies( - libraryId: number, + library: CoreH5PLibraryBeingSaved, dependencies: CoreH5PLibraryBasicData[], dependencyType: string, siteId?: string, @@ -761,9 +762,17 @@ export class CoreH5PFramework { // Get the ID of the library. const dependencyId = await this.getLibraryIdByData(dependency, siteId); + if (!dependencyId) { + // Missing dependency. It should have been detected before installing the package. + throw new CoreError(Translate.instant('core.h5p.missingdependency', { $a: { + lib: CoreH5PCore.libraryToString(library), + dep: CoreH5PCore.libraryToString(dependency), + } })); + } + // Create the relation. const entry: Partial = { - libraryid: libraryId, + libraryid: library.libraryId, requiredlibraryid: dependencyId, dependencytype: dependencyType, }; diff --git a/src/core/features/h5p/classes/helper.ts b/src/core/features/h5p/classes/helper.ts index bfb7b9537..23e447b93 100644 --- a/src/core/features/h5p/classes/helper.ts +++ b/src/core/features/h5p/classes/helper.ts @@ -211,7 +211,7 @@ export class CoreH5PHelper { // Read the contents of the unzipped dir, process them and store them. const contents = await CoreFile.getDirectoryContents(destFolder); - const filesData = await CoreH5P.h5pValidator.processH5PFiles(destFolder, contents); + const filesData = await CoreH5P.h5pValidator.processH5PFiles(destFolder, contents, siteId); const content = await CoreH5P.h5pStorage.savePackage(filesData, folderName, fileUrl, false, siteId); diff --git a/src/core/features/h5p/classes/storage.ts b/src/core/features/h5p/classes/storage.ts index 4fc3bcdb1..c79bdd305 100644 --- a/src/core/features/h5p/classes/storage.ts +++ b/src/core/features/h5p/classes/storage.ts @@ -119,24 +119,26 @@ export class CoreH5PStorage { return; } - const libId = libraryData.libraryId; - - libraryIds.push(libId); + libraryIds.push(libraryData.libraryId); // Remove any old dependencies. - await this.h5pFramework.deleteLibraryDependencies(libId, siteId); + await this.h5pFramework.deleteLibraryDependencies(libraryData.libraryId, siteId); // Insert the different new ones. const promises: Promise[] = []; if (typeof libraryData.preloadedDependencies != 'undefined') { - promises.push(this.h5pFramework.saveLibraryDependencies(libId, libraryData.preloadedDependencies, 'preloaded')); + promises.push(this.h5pFramework.saveLibraryDependencies( + libraryData, + libraryData.preloadedDependencies, + 'preloaded', + )); } if (typeof libraryData.dynamicDependencies != 'undefined') { - promises.push(this.h5pFramework.saveLibraryDependencies(libId, libraryData.dynamicDependencies, 'dynamic')); + promises.push(this.h5pFramework.saveLibraryDependencies(libraryData, libraryData.dynamicDependencies, 'dynamic')); } if (typeof libraryData.editorDependencies != 'undefined') { - promises.push(this.h5pFramework.saveLibraryDependencies(libId, libraryData.editorDependencies, 'editor')); + promises.push(this.h5pFramework.saveLibraryDependencies(libraryData, libraryData.editorDependencies, 'editor')); } await Promise.all(promises); diff --git a/src/core/features/h5p/classes/validator.ts b/src/core/features/h5p/classes/validator.ts index e3ec91d49..ac300f409 100644 --- a/src/core/features/h5p/classes/validator.ts +++ b/src/core/features/h5p/classes/validator.ts @@ -12,17 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CoreError } from '@classes/errors/error'; import { FileEntry, DirectoryEntry } from '@ionic-native/file/ngx'; import { CoreFile, CoreFileFormat } from '@services/file'; import { CoreTextUtils } from '@services/utils/text'; +import { Translate } from '@singletons'; import { CoreH5PSemantics } from './content-validator'; -import { CoreH5PCore, CoreH5PLibraryBasicData } from './core'; +import { CoreH5PCore, CoreH5PLibraryBasicData, CoreH5PMissingLibrary } from './core'; +import { CoreH5PFramework } from './framework'; /** * Equivalent to H5P's H5PValidator class. */ export class CoreH5PValidator { + constructor(public h5pFramework: CoreH5PFramework) { + } + /** * Get library data. * This function won't validate most things because it should've been done by the server already. @@ -49,6 +55,57 @@ export class CoreH5PValidator { return libraryData; } + /** + * Use the dependency declarations to find any missing libraries. + * + * @param libraries Libraries to check. + * @return Promise resolved with the missing dependencies. + */ + protected getMissingLibraries(libraries: CoreH5PLibrariesJsonData): Record { + const missing: Record = {}; + + Object.values(libraries).forEach((library) => { + if (typeof library.preloadedDependencies !== 'undefined') { + Object.assign(missing, this.getMissingDependencies(library.preloadedDependencies, library, libraries)); + } + if (typeof library.dynamicDependencies !== 'undefined') { + Object.assign(missing, this.getMissingDependencies(library.dynamicDependencies, library, libraries)); + } + if (typeof library.editorDependencies !== 'undefined') { + Object.assign(missing, this.getMissingDependencies(library.editorDependencies, library, libraries)); + } + }); + + return missing; + } + + /** + * Helper function for getMissingLibraries, searches for dependency required libraries in the provided list of libraries. + * + * @param dependencies Dependencies to check. + * @param library Library that has these dependencies. + * @param libraries Libraries. + * @return Promise resolved with missing dependencies. + */ + protected getMissingDependencies( + dependencies: CoreH5PLibraryBasicData[], + library: CoreH5PLibraryJsonData, + libraries: CoreH5PLibrariesJsonData, + ): Record { + const missing: Record = {}; + + dependencies.forEach((dependency) => { + const libString = CoreH5PCore.libraryToString(dependency); + if (!libraries[libString]) { + missing[libString] = Object.assign(dependency, { + libString: CoreH5PCore.libraryToString(library), + }); + } + }); + + return missing; + } + /** * Get library data for all libraries in an H5P package. * @@ -106,9 +163,14 @@ export class CoreH5PValidator { * * @param packagePath The path to the package folder. * @param entries List of files and directories in the root of the package folder. + * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - async processH5PFiles(packagePath: string, entries: (DirectoryEntry | FileEntry)[]): Promise { + async processH5PFiles( + packagePath: string, + entries: (DirectoryEntry | FileEntry)[], + siteId?: string, + ): Promise { // Read the needed files. const results = await Promise.all([ @@ -117,6 +179,31 @@ export class CoreH5PValidator { this.getPackageLibrariesData(packagePath, entries), ]); + // Check if there are missing libraries. + const missingLibraries = this.getMissingLibraries(results[2]); + + // Check if the missing libraries are already installed in the app. + await Promise.all(Object.keys(missingLibraries).map(async (libString) => { + const dependency = missingLibraries[libString]; + const dependencyId = await this.h5pFramework.getLibraryIdByData(dependency, siteId); + + if (dependencyId) { + // Lib is installed. + delete missingLibraries[libString]; + } + })); + + if (Object.keys(missingLibraries).length > 0) { + // Missing library, throw error. + const libString = Object.keys(missingLibraries)[0]; + const missingLibrary = missingLibraries[libString]; + + throw new CoreError(Translate.instant('core.h5p.missingdependency', { $a: { + lib: missingLibrary.libString, + dep: libString, + } })); + } + return { librariesJsonData: results[2], mainJsonData: results[0], diff --git a/src/core/features/h5p/lang.json b/src/core/features/h5p/lang.json index 19018057f..3f62f0325 100644 --- a/src/core/features/h5p/lang.json +++ b/src/core/features/h5p/lang.json @@ -59,6 +59,7 @@ "licensee": "Licensee", "licenseextras": "Licence extras", "licenseversion": "Licence version", + "missingdependency": "Missing dependency {{$a.dep}} required by {{$a.lib}}.", "nocopyright": "No copyright information available for this content.", "offlineDialogBody": "We were unable to send information about your completion of this task. Please check your internet connection.", "offlineDialogHeader": "Your connection to the server was lost", @@ -90,4 +91,4 @@ "years": "Year(s)", "yearsfrom": "Years (from)", "yearsto": "Years (to)" -} \ No newline at end of file +} diff --git a/src/core/features/h5p/services/h5p.ts b/src/core/features/h5p/services/h5p.ts index f4d9b27ac..92bf18bc8 100644 --- a/src/core/features/h5p/services/h5p.ts +++ b/src/core/features/h5p/services/h5p.ts @@ -48,8 +48,8 @@ export class CoreH5PProvider { constructor() { this.queueRunner = new CoreQueueRunner(1); - this.h5pValidator = new CoreH5PValidator(); this.h5pFramework = new CoreH5PFramework(); + this.h5pValidator = new CoreH5PValidator(this.h5pFramework); this.h5pCore = new CoreH5PCore(this.h5pFramework); this.h5pStorage = new CoreH5PStorage(this.h5pCore, this.h5pFramework); this.h5pPlayer = new CoreH5PPlayer(this.h5pCore, this.h5pStorage);