forked from EVOgeek/Vmeda.Online
		
	MOBILE-3666 h5p: Implement services and classes
This commit is contained in:
		
							parent
							
								
									c83ff34ae0
								
							
						
					
					
						commit
						f1ac735abf
					
				| @ -1087,7 +1087,7 @@ export class SQLiteDB { | ||||
| } | ||||
| 
 | ||||
| export type SQLiteDBRecordValues = { | ||||
|     [key in string ]: SQLiteDBRecordValue | undefined; | ||||
|     [key in string ]: SQLiteDBRecordValue | undefined | null; | ||||
| }; | ||||
| 
 | ||||
| export type SQLiteDBQueryParams = { | ||||
|  | ||||
| @ -18,6 +18,7 @@ import { CoreCourseModule } from './course/course.module'; | ||||
| import { CoreCoursesModule } from './courses/courses.module'; | ||||
| import { CoreEmulatorModule } from './emulator/emulator.module'; | ||||
| import { CoreFileUploaderModule } from './fileuploader/fileuploader.module'; | ||||
| import { CoreH5PModule } from './h5p/h5p.module'; | ||||
| import { CoreLoginModule } from './login/login.module'; | ||||
| import { CoreMainMenuModule } from './mainmenu/mainmenu.module'; | ||||
| import { CoreSettingsModule } from './settings/settings.module'; | ||||
| @ -41,6 +42,7 @@ import { CoreXAPIModule } from './xapi/xapi.module'; | ||||
|         CoreUserModule, | ||||
|         CorePushNotificationsModule, | ||||
|         CoreXAPIModule, | ||||
|         CoreH5PModule, | ||||
|     ], | ||||
| }) | ||||
| export class CoreFeaturesModule {} | ||||
|  | ||||
							
								
								
									
										1392
									
								
								src/core/features/h5p/classes/content-validator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1392
									
								
								src/core/features/h5p/classes/content-validator.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1005
									
								
								src/core/features/h5p/classes/core.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1005
									
								
								src/core/features/h5p/classes/core.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										475
									
								
								src/core/features/h5p/classes/file-storage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										475
									
								
								src/core/features/h5p/classes/file-storage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,475 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreFile } from '@services/file'; | ||||
| import { CoreFilepool } from '@services/filepool'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreMimetypeUtils } from '@services/utils/mimetype'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { | ||||
|     CoreH5PCore, | ||||
|     CoreH5PDependencyAsset, | ||||
|     CoreH5PContentDependencyData, | ||||
|     CoreH5PDependenciesFiles, | ||||
|     CoreH5PLibraryBasicData, | ||||
|     CoreH5PContentMainLibraryData, | ||||
| } from './core'; | ||||
| import { CONTENTS_LIBRARIES_TABLE_NAME, CONTENT_TABLE_NAME, CoreH5PLibraryCachedAssetsDBRecord } from '../services/database/h5p'; | ||||
| import { CoreH5PLibraryBeingSaved } from './storage'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to Moodle's implementation of H5PFileStorage. | ||||
|  */ | ||||
| export class CoreH5PFileStorage { | ||||
| 
 | ||||
|     static readonly CACHED_ASSETS_FOLDER_NAME = 'cachedassets'; | ||||
| 
 | ||||
|     /** | ||||
|      * Will concatenate all JavaScrips and Stylesheets into two files in order to improve page performance. | ||||
|      * | ||||
|      * @param files A set of all the assets required for content to display. | ||||
|      * @param key Hashed key for cached asset. | ||||
|      * @param folderName Name of the folder of the H5P package. | ||||
|      * @param siteId The site ID. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async cacheAssets(files: CoreH5PDependenciesFiles, key: string, folderName: string, siteId: string): Promise<void> { | ||||
| 
 | ||||
|         const cachedAssetsPath = this.getCachedAssetsFolderPath(folderName, siteId); | ||||
| 
 | ||||
|         // Treat each type in the assets.
 | ||||
|         await Promise.all(Object.keys(files).map(async (type) => { | ||||
| 
 | ||||
|             const assets: CoreH5PDependencyAsset[] = files[type]; | ||||
| 
 | ||||
|             if (!assets || !assets.length) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Create new file for cached assets.
 | ||||
|             const fileName = key + '.' + (type == 'scripts' ? 'js' : 'css'); | ||||
|             const path = CoreTextUtils.instance.concatenatePaths(cachedAssetsPath, fileName); | ||||
| 
 | ||||
|             // Store concatenated content.
 | ||||
|             const content = await this.concatenateFiles(assets, type); | ||||
| 
 | ||||
|             await CoreFile.instance.writeFile(path, content); | ||||
| 
 | ||||
|             // Now update the files data.
 | ||||
|             files[type] = [ | ||||
|                 { | ||||
|                     path: CoreTextUtils.instance.concatenatePaths(CoreH5PFileStorage.CACHED_ASSETS_FOLDER_NAME, fileName), | ||||
|                     version: '', | ||||
|                 }, | ||||
|             ]; | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Adds all files of a type into one file. | ||||
|      * | ||||
|      * @param assets A list of files. | ||||
|      * @param type The type of files in assets. Either 'scripts' or 'styles' | ||||
|      * @return Promise resolved with all of the files content in one string. | ||||
|      */ | ||||
|     protected async concatenateFiles(assets: CoreH5PDependencyAsset[], type: string): Promise<string> { | ||||
|         const basePath = CoreFile.instance.convertFileSrc(CoreFile.instance.getBasePathInstant()); | ||||
|         let content = ''; | ||||
| 
 | ||||
|         for (const i in assets) { | ||||
|             const asset = assets[i]; | ||||
| 
 | ||||
|             let fileContent = await CoreFile.instance.readFile(asset.path); | ||||
| 
 | ||||
|             if (type == 'scripts') { | ||||
|                 // No need to treat scripts, just append the content.
 | ||||
|                 content += fileContent + ';\n'; | ||||
| 
 | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             // Rewrite relative URLs used inside stylesheets.
 | ||||
|             const matches = fileContent.match(/url\(['"]?([^"')]+)['"]?\)/ig); | ||||
|             const assetPath = asset.path.replace(/(^\/|\/$)/g, ''); // Path without start/end slashes.
 | ||||
|             const treated = {}; | ||||
| 
 | ||||
|             if (matches && matches.length) { | ||||
|                 matches.forEach((match) => { | ||||
|                     let url = match.replace(/(url\(['"]?|['"]?\)$)/ig, ''); | ||||
| 
 | ||||
|                     if (treated[url] || url.match(/^(data:|([a-z0-9]+:)?\/)/i)) { | ||||
|                         return; // Not relative or already treated, skip.
 | ||||
|                     } | ||||
| 
 | ||||
|                     const pathSplit = assetPath.split('/'); | ||||
|                     treated[url] = url; | ||||
| 
 | ||||
|                     /* Find "../" in the URL. If it exists, we have to remove "../" and switch the last folder in the | ||||
|                         filepath for the first folder in the url. */ | ||||
|                     if (url.match(/^\.\.\//)) { | ||||
|                         // Split and remove empty values.
 | ||||
|                         const urlSplit = url.split('/').filter((i) => i); | ||||
| 
 | ||||
|                         // Remove the file name from the asset path.
 | ||||
|                         pathSplit.pop(); | ||||
| 
 | ||||
|                         // Remove the first element from the file URL: ../ .
 | ||||
|                         urlSplit.shift(); | ||||
| 
 | ||||
|                         // Put the url's first folder into the asset path.
 | ||||
|                         pathSplit[pathSplit.length - 1] = urlSplit[0]; | ||||
|                         urlSplit.shift(); | ||||
| 
 | ||||
|                         // Create the new URL and replace it in the file contents.
 | ||||
|                         url = pathSplit.join('/') + '/' + urlSplit.join('/'); | ||||
| 
 | ||||
|                     } else { | ||||
|                         pathSplit[pathSplit.length - 1] = url; // Put the whole path to the end of the asset path.
 | ||||
|                         url = pathSplit.join('/'); | ||||
|                     } | ||||
| 
 | ||||
|                     fileContent = fileContent.replace( | ||||
|                         new RegExp(CoreTextUtils.instance.escapeForRegex(match), 'g'), | ||||
|                         'url("' + CoreTextUtils.instance.concatenatePaths(basePath, url) + '")', | ||||
|                     ); | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             content += fileContent + '\n'; | ||||
|         } | ||||
| 
 | ||||
|         return content; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete cached assets from file system. | ||||
|      * | ||||
|      * @param libraryId Library identifier. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteCachedAssets(removedEntries: CoreH5PLibraryCachedAssetsDBRecord[], siteId?: string): Promise<void> { | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         removedEntries.forEach((entry) => { | ||||
|             const cachedAssetsFolder = this.getCachedAssetsFolderPath(entry.foldername, site.getId()); | ||||
| 
 | ||||
|             ['js', 'css'].forEach((type) => { | ||||
|                 const path = CoreTextUtils.instance.concatenatePaths(cachedAssetsFolder, entry.hash + '.' + type); | ||||
| 
 | ||||
|                 promises.push(CoreFile.instance.removeFile(path)); | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|         // Ignore errors, maybe there's no cached asset of some type.
 | ||||
|         await CoreUtils.instance.ignoreErrors(CoreUtils.instance.allPromises(promises)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes a content folder from the file system. | ||||
|      * | ||||
|      * @param folderName Folder name of the content. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteContentFolder(folderName: string, siteId: string): Promise<void> { | ||||
|         await CoreFile.instance.removeDir(this.getContentFolderPath(folderName, siteId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete content indexes from filesystem. | ||||
|      * | ||||
|      * @param folderName Name of the folder of the H5P package. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteContentIndex(folderName: string, siteId: string): Promise<void> { | ||||
|         await CoreFile.instance.removeFile(this.getContentIndexPath(folderName, siteId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete content indexes from filesystem. | ||||
|      * | ||||
|      * @param libraryId Library identifier. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteContentIndexesForLibrary(libraryId: number, siteId?: string): Promise<void> { | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const db = site.getDb(); | ||||
| 
 | ||||
|         // Get the folder names of all the packages that use this library.
 | ||||
|         const query = 'SELECT DISTINCT hc.foldername ' + | ||||
|                     'FROM ' + CONTENTS_LIBRARIES_TABLE_NAME + ' hcl ' + | ||||
|                     'JOIN ' + CONTENT_TABLE_NAME + ' hc ON hcl.h5pid = hc.id ' + | ||||
|                     'WHERE hcl.libraryid = ?'; | ||||
|         const queryArgs = [libraryId]; | ||||
| 
 | ||||
|         const result = await db.execute(query, queryArgs); | ||||
| 
 | ||||
|         await Array.from(result.rows).map(async (entry: {foldername: string}) => { | ||||
|             try { | ||||
|                 // Delete the index.html.
 | ||||
|                 await this.deleteContentIndex(entry.foldername, site.getId()); | ||||
|             } catch (error) { | ||||
|                 // Ignore errors.
 | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes a library from the file system. | ||||
|      * | ||||
|      * @param libraryData The library data. | ||||
|      * @param siteId Site ID. | ||||
|      * @param folderName Folder name. If not provided, it will be calculated. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteLibraryFolder( | ||||
|         libraryData: CoreH5PLibraryBasicData | CoreH5PContentMainLibraryData, | ||||
|         siteId: string, | ||||
|         folderName?: string, | ||||
|     ): Promise<void> { | ||||
|         await CoreFile.instance.removeDir(this.getLibraryFolderPath(libraryData, siteId, folderName)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Will check if there are cache assets available for content. | ||||
|      * | ||||
|      * @param key Hashed key for cached asset | ||||
|      * @return Promise resolved with the files. | ||||
|      */ | ||||
|     async getCachedAssets(key: string): Promise<{scripts?: CoreH5PDependencyAsset[]; styles?: CoreH5PDependencyAsset[]} | null> { | ||||
| 
 | ||||
|         // Get JS and CSS cached assets if they exist.
 | ||||
|         const results = await Promise.all([ | ||||
|             this.getCachedAsset(key, '.js'), | ||||
|             this.getCachedAsset(key, '.css'), | ||||
|         ]); | ||||
| 
 | ||||
|         const files = { | ||||
|             scripts: results[0], | ||||
|             styles: results[1], | ||||
|         }; | ||||
| 
 | ||||
|         return files.scripts || files.styles ? files : null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a cached asset file exists and, if so, return its data. | ||||
|      * | ||||
|      * @param key Key of the cached asset. | ||||
|      * @param extension Extension of the file to get. | ||||
|      * @return Promise resolved with the list of assets (only one), undefined if not found. | ||||
|      */ | ||||
|     protected async getCachedAsset(key: string, extension: string): Promise<CoreH5PDependencyAsset[] | undefined> { | ||||
| 
 | ||||
|         try { | ||||
|             const path = CoreTextUtils.instance.concatenatePaths(CoreH5PFileStorage.CACHED_ASSETS_FOLDER_NAME, key + extension); | ||||
| 
 | ||||
|             const size = await CoreFile.instance.getFileSize(path); | ||||
| 
 | ||||
|             if (size > 0) { | ||||
|                 return [ | ||||
|                     { | ||||
|                         path: path, | ||||
|                         version: '', | ||||
|                     }, | ||||
|                 ]; | ||||
|             } | ||||
|         } catch (error) { | ||||
|             // Not found, nothing to do.
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get relative path to a content cached assets. | ||||
|      * | ||||
|      * @param folderName Name of the folder of the content the assets belong to. | ||||
|      * @param siteId Site ID. | ||||
|      * @return Path. | ||||
|      */ | ||||
|     getCachedAssetsFolderPath(folderName: string, siteId: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths( | ||||
|             this.getContentFolderPath(folderName, siteId), | ||||
|             CoreH5PFileStorage.CACHED_ASSETS_FOLDER_NAME, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a content folder name given the package URL. | ||||
|      * | ||||
|      * @param fileUrl Package URL. | ||||
|      * @param siteId Site ID. | ||||
|      * @return Promise resolved with the folder name. | ||||
|      */ | ||||
|     async getContentFolderNameByUrl(fileUrl: string, siteId: string): Promise<string> { | ||||
|         const path = await CoreFilepool.instance.getFilePathByUrl(siteId, fileUrl); | ||||
| 
 | ||||
|         const fileAndDir = CoreFile.instance.getFileAndDirectoryFromPath(path); | ||||
| 
 | ||||
|         return CoreMimetypeUtils.instance.removeExtension(fileAndDir.name); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a package content path. | ||||
|      * | ||||
|      * @param folderName Name of the folder of the H5P package. | ||||
|      * @param siteId The site ID. | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getContentFolderPath(folderName: string, siteId: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths( | ||||
|             this.getExternalH5PFolderPath(siteId), | ||||
|             'packages/' + folderName + '/content', | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the content index file. | ||||
|      * | ||||
|      * @param fileUrl URL of the H5P package. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the file URL if exists, rejected otherwise. | ||||
|      */ | ||||
|     async getContentIndexFileUrl(fileUrl: string, siteId?: string): Promise<string> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const folderName = await this.getContentFolderNameByUrl(fileUrl, siteId); | ||||
| 
 | ||||
|         const file = await CoreFile.instance.getFile(this.getContentIndexPath(folderName, siteId)); | ||||
| 
 | ||||
|         return file.toURL(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the path to a content index. | ||||
|      * | ||||
|      * @param folderName Name of the folder of the H5P package. | ||||
|      * @param siteId The site ID. | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getContentIndexPath(folderName: string, siteId: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(this.getContentFolderPath(folderName, siteId), 'index.html'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the path to the folder that contains the H5P core libraries. | ||||
|      * | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getCoreH5PPath(): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(CoreFile.instance.getWWWPath(), '/h5p/'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the path to the dependency. | ||||
|      * | ||||
|      * @param dependency Dependency library. | ||||
|      * @return The path to the dependency library | ||||
|      */ | ||||
|     getDependencyPath(dependency: CoreH5PContentDependencyData): string { | ||||
|         return 'libraries/' + dependency.machineName + '-' + dependency.majorVersion + '.' + dependency.minorVersion; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get path to the folder containing H5P files extracted from packages. | ||||
|      * | ||||
|      * @param siteId The site ID. | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getExternalH5PFolderPath(siteId: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(CoreFile.instance.getSiteFolder(siteId), 'h5p'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get libraries folder path. | ||||
|      * | ||||
|      * @param siteId The site ID. | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getLibrariesFolderPath(siteId: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(this.getExternalH5PFolderPath(siteId), 'libraries'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library's folder path. | ||||
|      * | ||||
|      * @param libraryData The library data. | ||||
|      * @param siteId The site ID. | ||||
|      * @param folderName Folder name. If not provided, it will be calculated. | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getLibraryFolderPath( | ||||
|         libraryData: CoreH5PLibraryBasicData | CoreH5PContentMainLibraryData, | ||||
|         siteId: string, | ||||
|         folderName?: string, | ||||
|     ): string { | ||||
|         if (!folderName) { | ||||
|             folderName = CoreH5PCore.libraryToString(libraryData, true); | ||||
|         } | ||||
| 
 | ||||
|         return CoreTextUtils.instance.concatenatePaths(this.getLibrariesFolderPath(siteId), folderName); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save the content in filesystem. | ||||
|      * | ||||
|      * @param contentPath Path to the current content folder (tmp). | ||||
|      * @param folderName Name to put to the content folder. | ||||
|      * @param siteId Site ID. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async saveContent(contentPath: string, folderName: string, siteId: string): Promise<void> { | ||||
|         const folderPath = this.getContentFolderPath(folderName, siteId); | ||||
| 
 | ||||
|         // Delete existing content for this package.
 | ||||
|         await CoreUtils.instance.ignoreErrors(CoreFile.instance.removeDir(folderPath)); | ||||
| 
 | ||||
|         // Copy the new one.
 | ||||
|         await CoreFile.instance.moveDir(contentPath, folderPath); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save a library in filesystem. | ||||
|      * | ||||
|      * @param libraryData Library data. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async saveLibrary(libraryData: CoreH5PLibraryBeingSaved, siteId?: string): Promise<void> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const folderPath = this.getLibraryFolderPath(libraryData, siteId); | ||||
| 
 | ||||
|         // Delete existing library version.
 | ||||
|         try { | ||||
|             await CoreFile.instance.removeDir(folderPath); | ||||
|         } catch (error) { | ||||
|             // Ignore errors, maybe it doesn't exist.
 | ||||
|         } | ||||
| 
 | ||||
|         if (libraryData.uploadDirectory) { | ||||
|             // Copy the new one.
 | ||||
|             await CoreFile.instance.moveDir(libraryData.uploadDirectory, folderPath, true); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										917
									
								
								src/core/features/h5p/classes/framework.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										917
									
								
								src/core/features/h5p/classes/framework.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,917 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreH5P } from '@features/h5p/services/h5p'; | ||||
| import { | ||||
|     CoreH5PCore, | ||||
|     CoreH5PDisplayOptionBehaviour, | ||||
|     CoreH5PContentDependencyData, | ||||
|     CoreH5PLibraryData, | ||||
|     CoreH5PLibraryAddonData, | ||||
|     CoreH5PContentDepsTreeDependency, | ||||
|     CoreH5PLibraryBasicData, | ||||
|     CoreH5PLibraryBasicDataWithPatch, | ||||
| } from './core'; | ||||
| import { | ||||
|     CONTENT_TABLE_NAME, | ||||
|     LIBRARIES_CACHEDASSETS_TABLE_NAME, | ||||
|     CoreH5PLibraryCachedAssetsDBRecord, | ||||
|     LIBRARIES_TABLE_NAME, | ||||
|     LIBRARY_DEPENDENCIES_TABLE_NAME, | ||||
|     CONTENTS_LIBRARIES_TABLE_NAME, | ||||
|     CoreH5PContentDBRecord, | ||||
|     CoreH5PLibraryDBRecord, | ||||
|     CoreH5PLibraryDependencyDBRecord, | ||||
|     CoreH5PContentsLibraryDBRecord, | ||||
| } from '../services/database/h5p'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { CoreH5PSemantics } from './content-validator'; | ||||
| import { CoreH5PContentBeingSaved, CoreH5PLibraryBeingSaved } from './storage'; | ||||
| import { CoreH5PLibraryAddTo } from './validator'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to Moodle's implementation of H5PFrameworkInterface. | ||||
|  */ | ||||
| export class CoreH5PFramework { | ||||
| 
 | ||||
|     /** | ||||
|      * Will clear filtered params for all the content that uses the specified libraries. | ||||
|      * This means that the content dependencies will have to be rebuilt and the parameters re-filtered. | ||||
|      * | ||||
|      * @param libraryIds Array of library ids. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async clearFilteredParameters(libraryIds: number[], siteId?: string): Promise<void> { | ||||
|         if (!libraryIds || !libraryIds.length) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const whereAndParams = db.getInOrEqual(libraryIds); | ||||
|         whereAndParams[0] = 'mainlibraryid ' + whereAndParams[0]; | ||||
| 
 | ||||
|         await db.updateRecordsWhere(CONTENT_TABLE_NAME, { filtered: null }, whereAndParams[0], whereAndParams[1]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete cached assets from DB. | ||||
|      * | ||||
|      * @param libraryId Library identifier. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the removed entries. | ||||
|      */ | ||||
|     async deleteCachedAssets(libraryId: number, siteId?: string): Promise<CoreH5PLibraryCachedAssetsDBRecord[]> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         // Get all the hashes that use this library.
 | ||||
|         const entries = await db.getRecords<CoreH5PLibraryCachedAssetsDBRecord>( | ||||
|             LIBRARIES_CACHEDASSETS_TABLE_NAME, | ||||
|             { libraryid: libraryId }, | ||||
|         ); | ||||
| 
 | ||||
|         const hashes = entries.map((entry) => entry.hash); | ||||
| 
 | ||||
|         if (hashes.length) { | ||||
|             // Delete the entries from DB.
 | ||||
|             await db.deleteRecordsList(LIBRARIES_CACHEDASSETS_TABLE_NAME, 'hash', hashes); | ||||
|         } | ||||
| 
 | ||||
|         return entries; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete content data from DB. | ||||
|      * | ||||
|      * @param id Content ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteContentData(id: number, siteId?: string): Promise<void> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await Promise.all([ | ||||
|             // Delete the content data.
 | ||||
|             db.deleteRecords(CONTENT_TABLE_NAME, { id }), | ||||
| 
 | ||||
|             // Remove content library dependencies.
 | ||||
|             this.deleteLibraryUsage(id, siteId), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete library data from DB. | ||||
|      * | ||||
|      * @param id Library ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteLibrary(id: number, siteId?: string): Promise<void> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await db.deleteRecords(LIBRARIES_TABLE_NAME, { id }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete all dependencies belonging to given library. | ||||
|      * | ||||
|      * @param libraryId Library ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteLibraryDependencies(libraryId: number, siteId?: string): Promise<void> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await db.deleteRecords(LIBRARY_DEPENDENCIES_TABLE_NAME, { libraryid: libraryId }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete what libraries a content item is using. | ||||
|      * | ||||
|      * @param id Package ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteLibraryUsage(id: number, siteId?: string): Promise<void> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await db.deleteRecords(CONTENTS_LIBRARIES_TABLE_NAME, { h5pid: id }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all conent data from DB. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the list of content data. | ||||
|      */ | ||||
|     async getAllContentData(siteId?: string): Promise<CoreH5PContentDBRecord[]> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         return db.getAllRecords<CoreH5PContentDBRecord>(CONTENT_TABLE_NAME); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get conent data from DB. | ||||
|      * | ||||
|      * @param id Content ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the content data. | ||||
|      */ | ||||
|     async getContentData(id: number, siteId?: string): Promise<CoreH5PContentDBRecord> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         return db.getRecord<CoreH5PContentDBRecord>(CONTENT_TABLE_NAME, { id }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get conent data from DB. | ||||
|      * | ||||
|      * @param fileUrl H5P file URL. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the content data. | ||||
|      */ | ||||
|     async getContentDataByUrl(fileUrl: string, siteId?: string): Promise<CoreH5PContentDBRecord> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const db = site.getDb(); | ||||
| 
 | ||||
|         // Try to use the folder name, it should be more reliable than the URL.
 | ||||
|         const folderName = await CoreH5P.instance.h5pCore.h5pFS.getContentFolderNameByUrl(fileUrl, site.getId()); | ||||
| 
 | ||||
|         try { | ||||
|             return await db.getRecord<CoreH5PContentDBRecord>(CONTENT_TABLE_NAME, { foldername: folderName }); | ||||
|         } catch (error) { | ||||
|             // Cannot get folder name, the h5p file was probably deleted. Just use the URL.
 | ||||
|             return db.getRecord<CoreH5PContentDBRecord>(CONTENT_TABLE_NAME, { fileurl: fileUrl }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the latest library version. | ||||
|      * | ||||
|      * @param machineName The library's machine name. | ||||
|      * @return Promise resolved with the latest library version data. | ||||
|      */ | ||||
|     async getLatestLibraryVersion(machineName: string, siteId?: string): Promise<CoreH5PLibraryParsedDBRecord> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         try { | ||||
|             const records = await db.getRecords<CoreH5PLibraryDBRecord>( | ||||
|                 LIBRARIES_TABLE_NAME, | ||||
|                 { machinename: machineName }, | ||||
|                 'majorversion DESC, minorversion DESC, patchversion DESC', | ||||
|                 '*', | ||||
|                 0, | ||||
|                 1, | ||||
|             ); | ||||
| 
 | ||||
|             if (records && records[0]) { | ||||
|                 return this.parseLibDBData(records[0]); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             // Library not found.
 | ||||
|         } | ||||
| 
 | ||||
|         throw new CoreError(`Missing required library: ${machineName}`); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library data stored in DB. | ||||
|      * | ||||
|      * @param machineName Machine name. | ||||
|      * @param majorVersion Major version number. | ||||
|      * @param minorVersion Minor version number. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library data, rejected if not found. | ||||
|      */ | ||||
|     protected async getLibrary( | ||||
|         machineName: string, | ||||
|         majorVersion?: string | number, | ||||
|         minorVersion?: string | number, | ||||
|         siteId?: string, | ||||
|     ): Promise<CoreH5PLibraryParsedDBRecord> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const libraries = await db.getRecords<CoreH5PLibraryDBRecord>(LIBRARIES_TABLE_NAME, { | ||||
|             machinename: machineName, | ||||
|             majorversion: majorVersion, | ||||
|             minorversion: minorVersion, | ||||
|         }); | ||||
| 
 | ||||
|         if (!libraries.length) { | ||||
|             throw new CoreError('Libary not found.'); | ||||
|         } | ||||
| 
 | ||||
|         return this.parseLibDBData(libraries[0]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library data stored in DB. | ||||
|      * | ||||
|      * @param libraryData Library data. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library data, rejected if not found. | ||||
|      */ | ||||
|     getLibraryByData(libraryData: CoreH5PLibraryBasicData, siteId?: string): Promise<CoreH5PLibraryParsedDBRecord> { | ||||
|         return this.getLibrary(libraryData.machineName, libraryData.majorVersion, libraryData.minorVersion, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library data stored in DB by ID. | ||||
|      * | ||||
|      * @param id Library ID. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library data, rejected if not found. | ||||
|      */ | ||||
|     async getLibraryById(id: number, siteId?: string): Promise<CoreH5PLibraryParsedDBRecord> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const library = await db.getRecord<CoreH5PLibraryDBRecord>(LIBRARIES_TABLE_NAME, { id }); | ||||
| 
 | ||||
|         return this.parseLibDBData(library); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library ID. If not found, return null. | ||||
|      * | ||||
|      * @param machineName Machine name. | ||||
|      * @param majorVersion Major version number. | ||||
|      * @param minorVersion Minor version number. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library ID, null if not found. | ||||
|      */ | ||||
|     async getLibraryId( | ||||
|         machineName: string, | ||||
|         majorVersion?: string | number, | ||||
|         minorVersion?: string | number, | ||||
|         siteId?: string, | ||||
|     ): Promise<number | undefined> { | ||||
|         try { | ||||
|             const library = await this.getLibrary(machineName, majorVersion, minorVersion, siteId); | ||||
| 
 | ||||
|             return library.id || undefined; | ||||
|         } catch (error) { | ||||
|             return undefined; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library ID. If not found, return null. | ||||
|      * | ||||
|      * @param libraryData Library data. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library ID, null if not found. | ||||
|      */ | ||||
|     getLibraryIdByData(libraryData: CoreH5PLibraryBasicData, siteId?: string): Promise<number | undefined> { | ||||
|         return this.getLibraryId(libraryData.machineName, libraryData.majorVersion, libraryData.minorVersion, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the default behaviour for the display option defined. | ||||
|      * | ||||
|      * @param name Identifier for the setting. | ||||
|      * @param defaultValue Optional default value if settings is not set. | ||||
|      * @return Return the value for this display option. | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     getOption(name: string, defaultValue: unknown): unknown { | ||||
|         // For now, all them are disabled by default, so only will be rendered when defined in the display options.
 | ||||
|         return CoreH5PDisplayOptionBehaviour.CONTROLLED_BY_AUTHOR_DEFAULT_OFF; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether the user has permission to execute an action. | ||||
|      * | ||||
|      * @param permission Permission to check. | ||||
|      * @param id H5P package id. | ||||
|      * @return Whether the user has permission to execute an action. | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     hasPermission(permission: number, id: number): boolean { | ||||
|         // H5P capabilities have not been introduced.
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Determines if content slug is used. | ||||
|      * | ||||
|      * @param slug The content slug. | ||||
|      * @return Whether the content slug is used | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     isContentSlugAvailable(slug: string): boolean { | ||||
|         // By default the slug should be available as it's currently generated as a unique value for each h5p content.
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether a library is a patched version of the one installed. | ||||
|      * | ||||
|      * @param library Library to check. | ||||
|      * @param dbData Installed library. If not supplied it will be calculated. | ||||
|      * @return Promise resolved with boolean: whether it's a patched library. | ||||
|      */ | ||||
|     async isPatchedLibrary(library: CoreH5PLibraryBasicDataWithPatch, dbData?: CoreH5PLibraryParsedDBRecord): Promise<boolean> { | ||||
|         if (!dbData) { | ||||
|             dbData = await this.getLibraryByData(library); | ||||
|         } | ||||
| 
 | ||||
|         return library.patchVersion > dbData.patchversion; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convert list of library parameter values to csv. | ||||
|      * | ||||
|      * @param libraryData Library data as found in library.json files. | ||||
|      * @param key Key that should be found in libraryData. | ||||
|      * @param searchParam The library parameter (Default: 'path'). | ||||
|      * @return Library parameter values separated by ', ' | ||||
|      */ | ||||
|     libraryParameterValuesToCsv(libraryData: CoreH5PLibraryBeingSaved, key: string, searchParam: string = 'path'): string { | ||||
|         if (typeof libraryData[key] != 'undefined') { | ||||
|             const parameterValues: string[] = []; | ||||
| 
 | ||||
|             libraryData[key].forEach((file) => { | ||||
|                 for (const index in file) { | ||||
|                     if (index === searchParam) { | ||||
|                         parameterValues.push(file[index]); | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             return parameterValues.join(','); | ||||
|         } | ||||
| 
 | ||||
|         return ''; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load addon libraries. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the addon libraries. | ||||
|      */ | ||||
|     async loadAddons(siteId?: string): Promise<CoreH5PLibraryAddonData[]> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const query = 'SELECT l1.id AS libraryId, l1.machinename AS machineName, ' + | ||||
|                         'l1.majorversion AS majorVersion, l1.minorversion AS minorVersion, ' + | ||||
|                         'l1.patchversion AS patchVersion, l1.addto AS addTo, ' + | ||||
|                         'l1.preloadedjs AS preloadedJs, l1.preloadedcss AS preloadedCss ' + | ||||
|                     'FROM ' + LIBRARIES_TABLE_NAME + ' l1 ' + | ||||
|                     'JOIN ' + LIBRARIES_TABLE_NAME + ' l2 ON l1.machinename = l2.machinename AND (' + | ||||
|                         'l1.majorversion < l2.majorversion OR (l1.majorversion = l2.majorversion AND ' + | ||||
|                         'l1.minorversion < l2.minorversion)) ' + | ||||
|                     'WHERE l1.addto IS NOT NULL AND l2.machinename IS NULL'; | ||||
| 
 | ||||
|         const result = await db.execute(query); | ||||
| 
 | ||||
|         const addons: CoreH5PLibraryAddonData[] = []; | ||||
| 
 | ||||
|         for (let i = 0; i < result.rows.length; i++) { | ||||
|             addons.push(this.parseLibAddonData(result.rows.item(i))); | ||||
|         } | ||||
| 
 | ||||
|         return addons; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load content data from DB. | ||||
|      * | ||||
|      * @param id Content ID. | ||||
|      * @param fileUrl H5P file URL. Required if id is not provided. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the content data. | ||||
|      */ | ||||
|     async loadContent(id?: number, fileUrl?: string, siteId?: string): Promise<CoreH5PFrameworkContentData> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         let contentData: CoreH5PContentDBRecord; | ||||
| 
 | ||||
|         if (id) { | ||||
|             contentData = await this.getContentData(id, siteId); | ||||
|         } else if (fileUrl) { | ||||
|             contentData = await this.getContentDataByUrl(fileUrl, siteId); | ||||
|         } else { | ||||
|             throw new CoreError('No id or fileUrl supplied to loadContent.'); | ||||
|         } | ||||
| 
 | ||||
|         // Load the main library data.
 | ||||
|         const libData = await this.getLibraryById(contentData.mainlibraryid, siteId); | ||||
| 
 | ||||
|         // Map the values to the names used by the H5P core (it's the same Moodle web does).
 | ||||
|         const content = { | ||||
|             id: contentData.id, | ||||
|             params: contentData.jsoncontent, | ||||
|             embedType: 'iframe', // Always use iframe.
 | ||||
|             disable: null, | ||||
|             folderName: contentData.foldername, | ||||
|             title: libData.title, | ||||
|             slug: CoreH5PCore.slugify(libData.title) + '-' + contentData.id, | ||||
|             filtered: contentData.filtered, | ||||
|             libraryId: libData.id, | ||||
|             libraryName: libData.machinename, | ||||
|             libraryMajorVersion: libData.majorversion, | ||||
|             libraryMinorVersion: libData.minorversion, | ||||
|             libraryEmbedTypes: libData.embedtypes, | ||||
|             libraryFullscreen: libData.fullscreen, | ||||
|             metadata: null, | ||||
|         }; | ||||
| 
 | ||||
|         // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||
|         const params = CoreTextUtils.instance.parseJSON<any>(contentData.jsoncontent); | ||||
|         if (!params.metadata) { | ||||
|             params.metadata = {}; | ||||
|         } | ||||
|         content.metadata = params.metadata; | ||||
|         content.params = JSON.stringify(typeof params.params != 'undefined' && params.params != null ? params.params : params); | ||||
| 
 | ||||
|         return content; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load dependencies for the given content of the given type. | ||||
|      * | ||||
|      * @param id Content ID. | ||||
|      * @param type The dependency type. | ||||
|      * @return Content dependencies, indexed by machine name. | ||||
|      */ | ||||
|     async loadContentDependencies( | ||||
|         id: number, | ||||
|         type?: string, | ||||
|         siteId?: string, | ||||
|     ): Promise<{[machineName: string]: CoreH5PContentDependencyData}> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         let query = 'SELECT hl.id AS libraryId, hl.machinename AS machineName, ' + | ||||
|                         'hl.majorversion AS majorVersion, hl.minorversion AS minorVersion, ' + | ||||
|                         'hl.patchversion AS patchVersion, hl.preloadedcss AS preloadedCss, ' + | ||||
|                         'hl.preloadedjs AS preloadedJs, hcl.dropcss AS dropCss, ' + | ||||
|                         'hcl.dependencytype as dependencyType ' + | ||||
|                     'FROM ' + CONTENTS_LIBRARIES_TABLE_NAME + ' hcl ' + | ||||
|                     'JOIN ' + LIBRARIES_TABLE_NAME + ' hl ON hcl.libraryid = hl.id ' + | ||||
|                     'WHERE hcl.h5pid = ?'; | ||||
| 
 | ||||
|         const queryArgs: (string | number)[] = []; | ||||
|         queryArgs.push(id); | ||||
| 
 | ||||
|         if (type) { | ||||
|             query += ' AND hcl.dependencytype = ?'; | ||||
|             queryArgs.push(type); | ||||
|         } | ||||
| 
 | ||||
|         query += ' ORDER BY hcl.weight'; | ||||
| 
 | ||||
|         const result = await db.execute(query, queryArgs); | ||||
| 
 | ||||
|         const dependencies: {[machineName: string]: CoreH5PContentDependencyData} = {}; | ||||
| 
 | ||||
|         for (let i = 0; i < result.rows.length; i++) { | ||||
|             const dependency = result.rows.item(i); | ||||
| 
 | ||||
|             dependencies[dependency.machineName] = dependency; | ||||
|         } | ||||
| 
 | ||||
|         return dependencies; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Loads a library and its dependencies. | ||||
|      * | ||||
|      * @param machineName The library's machine name. | ||||
|      * @param majorVersion The library's major version. | ||||
|      * @param minorVersion The library's minor version. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library data. | ||||
|      */ | ||||
|     async loadLibrary( | ||||
|         machineName: string, | ||||
|         majorVersion: number, | ||||
|         minorVersion: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<CoreH5PLibraryData> { | ||||
| 
 | ||||
|         // First get the library data from DB.
 | ||||
|         const library = await this.getLibrary(machineName, majorVersion, minorVersion, siteId); | ||||
| 
 | ||||
|         const libraryData: CoreH5PLibraryData = { | ||||
|             libraryId: library.id, | ||||
|             title: library.title, | ||||
|             machineName: library.machinename, | ||||
|             majorVersion: library.majorversion, | ||||
|             minorVersion: library.minorversion, | ||||
|             patchVersion: library.patchversion, | ||||
|             runnable: library.runnable, | ||||
|             fullscreen: library.fullscreen, | ||||
|             embedTypes: library.embedtypes, | ||||
|             preloadedJs: library.preloadedjs || undefined, | ||||
|             preloadedCss: library.preloadedcss || undefined, | ||||
|             dropLibraryCss: library.droplibrarycss || undefined, | ||||
|             semantics: library.semantics || undefined, | ||||
|             preloadedDependencies: [], | ||||
|             dynamicDependencies: [], | ||||
|             editorDependencies: [], | ||||
|         }; | ||||
| 
 | ||||
|         // Now get the dependencies.
 | ||||
|         const sql = 'SELECT hl.id, hl.machinename, hl.majorversion, hl.minorversion, hll.dependencytype ' + | ||||
|                 'FROM ' + LIBRARY_DEPENDENCIES_TABLE_NAME + ' hll ' + | ||||
|                 'JOIN ' + LIBRARIES_TABLE_NAME + ' hl ON hll.requiredlibraryid = hl.id ' + | ||||
|                 'WHERE hll.libraryid = ? ' + | ||||
|                 'ORDER BY hl.id ASC'; | ||||
| 
 | ||||
|         const sqlParams = [ | ||||
|             library.id, | ||||
|         ]; | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const result = await db.execute(sql, sqlParams); | ||||
| 
 | ||||
|         for (let i = 0; i < result.rows.length; i++) { | ||||
|             const dependency: LibraryDependency = result.rows.item(i); | ||||
|             const key = dependency.dependencytype + 'Dependencies'; | ||||
| 
 | ||||
|             libraryData[key].push({ | ||||
|                 machineName: dependency.machinename, | ||||
|                 majorVersion: dependency.majorversion, | ||||
|                 minorVersion: dependency.minorversion, | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         return libraryData; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parse library addon data. | ||||
|      * | ||||
|      * @param library Library addon data. | ||||
|      * @return Parsed library. | ||||
|      */ | ||||
|     parseLibAddonData(library: LibraryAddonDBData): CoreH5PLibraryAddonData { | ||||
|         const parsedLib = <CoreH5PLibraryAddonData> library; | ||||
|         parsedLib.addTo = CoreTextUtils.instance.parseJSON<CoreH5PLibraryAddTo | null>(library.addTo, null); | ||||
| 
 | ||||
|         return parsedLib; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parse library DB data. | ||||
|      * | ||||
|      * @param library Library DB data. | ||||
|      * @return Parsed library. | ||||
|      */ | ||||
|     protected parseLibDBData(library: CoreH5PLibraryDBRecord): CoreH5PLibraryParsedDBRecord { | ||||
|         return Object.assign(library, { | ||||
|             semantics: library.semantics ? CoreTextUtils.instance.parseJSON(library.semantics, null) : null, | ||||
|             addto: library.addto ? CoreTextUtils.instance.parseJSON(library.addto, null) : null, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Resets marked user data for the given content. | ||||
|      * | ||||
|      * @param contentId Content ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     async resetContentUserData(conentId: number, siteId?: string): Promise<void> { | ||||
|         // Currently, we do not store user data for a content.
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Stores hash keys for cached assets, aggregated JavaScripts and stylesheets, and connects it to libraries so that we | ||||
|      * know which cache file to delete when a library is updated. | ||||
|      * | ||||
|      * @param key Hash key for the given libraries. | ||||
|      * @param libraries List of dependencies used to create the key. | ||||
|      * @param folderName The name of the folder that contains the H5P. | ||||
|      * @param siteId The site ID. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async saveCachedAssets( | ||||
|         hash: string, | ||||
|         dependencies: {[machineName: string]: CoreH5PContentDependencyData}, | ||||
|         folderName: string, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await Promise.all(Object.keys(dependencies).map(async (key) => { | ||||
|             const data: Partial<CoreH5PLibraryCachedAssetsDBRecord> = { | ||||
|                 hash: key, | ||||
|                 libraryid: dependencies[key].libraryId, | ||||
|                 foldername: folderName, | ||||
|             }; | ||||
| 
 | ||||
|             await db.insertRecord(LIBRARIES_CACHEDASSETS_TABLE_NAME, data); | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save library data in DB. | ||||
|      * | ||||
|      * @param libraryData Library data to save. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async saveLibraryData(libraryData: CoreH5PLibraryBeingSaved, siteId?: string): Promise<void> { | ||||
|         // Some special properties needs some checking and converting before they can be saved.
 | ||||
|         const preloadedJS = this.libraryParameterValuesToCsv(libraryData, 'preloadedJs', 'path'); | ||||
|         const preloadedCSS = this.libraryParameterValuesToCsv(libraryData, 'preloadedCss', 'path'); | ||||
|         const dropLibraryCSS = this.libraryParameterValuesToCsv(libraryData, 'dropLibraryCss', 'machineName'); | ||||
| 
 | ||||
|         if (typeof libraryData.semantics == 'undefined') { | ||||
|             libraryData.semantics = []; | ||||
|         } | ||||
|         if (typeof libraryData.fullscreen == 'undefined') { | ||||
|             libraryData.fullscreen = 0; | ||||
|         } | ||||
| 
 | ||||
|         let embedTypes = ''; | ||||
|         if (typeof libraryData.embedTypes != 'undefined') { | ||||
|             embedTypes = libraryData.embedTypes.join(', '); | ||||
|         } | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const db = site.getDb(); | ||||
|         const data: Partial<CoreH5PLibraryDBRecord> = { | ||||
|             title: libraryData.title, | ||||
|             machinename: libraryData.machineName, | ||||
|             majorversion: libraryData.majorVersion, | ||||
|             minorversion: libraryData.minorVersion, | ||||
|             patchversion: libraryData.patchVersion, | ||||
|             runnable: libraryData.runnable, | ||||
|             fullscreen: libraryData.fullscreen, | ||||
|             embedtypes: embedTypes, | ||||
|             preloadedjs: preloadedJS, | ||||
|             preloadedcss: preloadedCSS, | ||||
|             droplibrarycss: dropLibraryCSS, | ||||
|             semantics: typeof libraryData.semantics != 'undefined' ? JSON.stringify(libraryData.semantics) : null, | ||||
|             addto: typeof libraryData.addTo != 'undefined' ? JSON.stringify(libraryData.addTo) : null, | ||||
|         }; | ||||
| 
 | ||||
|         if (libraryData.libraryId) { | ||||
|             data.id = libraryData.libraryId; | ||||
|         } | ||||
| 
 | ||||
|         await db.insertRecord(LIBRARIES_TABLE_NAME, data); | ||||
| 
 | ||||
|         if (!data.id) { | ||||
|             // New library. Get its ID.
 | ||||
|             const entry = await db.getRecord<CoreH5PLibraryDBRecord>(LIBRARIES_TABLE_NAME, data); | ||||
| 
 | ||||
|             libraryData.libraryId = entry.id; | ||||
|         } else { | ||||
|             // Updated libary. Remove old dependencies.
 | ||||
|             await this.deleteLibraryDependencies(data.id, site.getId()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save what libraries a library is depending on. | ||||
|      * | ||||
|      * @param libraryId Library Id 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, | ||||
|         dependencies: CoreH5PLibraryBasicData[], | ||||
|         dependencyType: string, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await Promise.all(dependencies.map(async (dependency) => { | ||||
|             // Get the ID of the library.
 | ||||
|             const dependencyId = await this.getLibraryIdByData(dependency, siteId); | ||||
| 
 | ||||
|             // Create the relation.
 | ||||
|             const entry: Partial<CoreH5PLibraryDependencyDBRecord> = { | ||||
|                 libraryid: libraryId, | ||||
|                 requiredlibraryid: dependencyId, | ||||
|                 dependencytype: dependencyType, | ||||
|             }; | ||||
| 
 | ||||
|             await db.insertRecord(LIBRARY_DEPENDENCIES_TABLE_NAME, entry); | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Saves what libraries the content uses. | ||||
|      * | ||||
|      * @param id Id identifying the package. | ||||
|      * @param librariesInUse List of libraries the content uses. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async saveLibraryUsage( | ||||
|         id: number, | ||||
|         librariesInUse: {[key: string]: CoreH5PContentDepsTreeDependency}, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         // Calculate the CSS to drop.
 | ||||
|         const dropLibraryCssList: Record<string, string> = {}; | ||||
| 
 | ||||
|         for (const key in librariesInUse) { | ||||
|             const dependency = librariesInUse[key]; | ||||
| 
 | ||||
|             if ('dropLibraryCss' in dependency.library && dependency.library.dropLibraryCss) { | ||||
|                 const split = dependency.library.dropLibraryCss.split(', '); | ||||
| 
 | ||||
|                 split.forEach((css) => { | ||||
|                     dropLibraryCssList[css] = css; | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Now save the uusage.
 | ||||
|         await Promise.all(Object.keys(librariesInUse).map((key) => { | ||||
|             const dependency = librariesInUse[key]; | ||||
|             const data: Partial<CoreH5PContentsLibraryDBRecord> = { | ||||
|                 h5pid: id, | ||||
|                 libraryid: dependency.library.libraryId, | ||||
|                 dependencytype: dependency.type, | ||||
|                 dropcss: dropLibraryCssList[dependency.library.machineName] ? 1 : 0, | ||||
|                 weight: dependency.weight, | ||||
|             }; | ||||
| 
 | ||||
|             return db.insertRecord(CONTENTS_LIBRARIES_TABLE_NAME, data); | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save content data in DB and clear cache. | ||||
|      * | ||||
|      * @param content Content to save. | ||||
|      * @param folderName The name of the folder that contains the H5P. | ||||
|      * @param fileUrl The online URL of the package. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with content ID. | ||||
|      */ | ||||
|     async updateContent(content: CoreH5PContentBeingSaved, folderName: string, fileUrl: string, siteId?: string): Promise<number> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         // If the libraryid declared in the package is empty, get the latest version.
 | ||||
|         if (content.library && typeof content.library.libraryId == 'undefined') { | ||||
|             const mainLibrary = await this.getLatestLibraryVersion(content.library.machineName, siteId); | ||||
| 
 | ||||
|             content.library.libraryId = mainLibrary.id; | ||||
|         } | ||||
| 
 | ||||
|         const data: Partial<CoreH5PContentDBRecord> = { | ||||
|             id: undefined, | ||||
|             jsoncontent: content.params, | ||||
|             mainlibraryid: content.library?.libraryId, | ||||
|             timemodified: Date.now(), | ||||
|             filtered: null, | ||||
|             foldername: folderName, | ||||
|             fileurl: fileUrl, | ||||
|             timecreated: undefined, | ||||
|         }; | ||||
| 
 | ||||
|         if (typeof content.id != 'undefined') { | ||||
|             data.id = content.id; | ||||
|         } else { | ||||
|             data.timecreated = data.timemodified; | ||||
|         } | ||||
| 
 | ||||
|         await db.insertRecord(CONTENT_TABLE_NAME, data); | ||||
| 
 | ||||
|         if (!data.id) { | ||||
|             // New content. Get its ID.
 | ||||
|             const entry = await db.getRecord<CoreH5PContentDBRecord>(CONTENT_TABLE_NAME, data); | ||||
| 
 | ||||
|             content.id = entry.id; | ||||
|         } | ||||
| 
 | ||||
|         return content.id!; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This will update selected fields on the given content. | ||||
|      * | ||||
|      * @param id Content identifier. | ||||
|      * @param fields Object with the fields to update. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      */ | ||||
|     async updateContentFields(id: number, fields: Partial<CoreH5PContentDBRecord>, siteId?: string): Promise<void> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const data = Object.assign({}, fields); | ||||
| 
 | ||||
|         await db.updateRecords(CONTENT_TABLE_NAME, data, { id }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Content data returned by loadContent. | ||||
|  */ | ||||
| export type CoreH5PFrameworkContentData = { | ||||
|     id: number; // The id of the content.
 | ||||
|     params: string; // The content in json format.
 | ||||
|     embedType: string; // Embed type to use.
 | ||||
|     disable: number | null; // H5P Button display options.
 | ||||
|     folderName: string; // Name of the folder that contains the contents.
 | ||||
|     title: string; // Main library's title.
 | ||||
|     slug: string; // Lib title and ID slugified.
 | ||||
|     filtered: string | null; // Filtered version of json_content.
 | ||||
|     libraryId: number; // Main library's ID.
 | ||||
|     libraryName: string; // Main library's machine name.
 | ||||
|     libraryMajorVersion: number; // Main library's major version.
 | ||||
|     libraryMinorVersion: number; // Main library's minor version.
 | ||||
|     libraryEmbedTypes: string; // Main library's list of supported embed types.
 | ||||
|     libraryFullscreen: number; // Main library's display fullscreen button.
 | ||||
|     metadata: unknown; // Content metadata.
 | ||||
| }; | ||||
| 
 | ||||
| export type CoreH5PLibraryParsedDBRecord = Omit<CoreH5PLibraryDBRecord, 'semantics'|'addto'> & { | ||||
|     semantics: CoreH5PSemantics[] | null; | ||||
|     addto: CoreH5PLibraryAddTo | null; | ||||
| }; | ||||
| 
 | ||||
| type LibraryDependency = { | ||||
|     id: number; | ||||
|     machinename: string; | ||||
|     majorversion: number; | ||||
|     minorversion: number; | ||||
|     dependencytype: string; | ||||
| }; | ||||
| 
 | ||||
| type LibraryAddonDBData = Omit<CoreH5PLibraryAddonData, 'addTo'> & { | ||||
|     addTo: string; | ||||
| }; | ||||
| 
 | ||||
							
								
								
									
										255
									
								
								src/core/features/h5p/classes/helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								src/core/features/h5p/classes/helper.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,255 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { FileEntry } from '@ionic-native/file'; | ||||
| 
 | ||||
| import { CoreFile, CoreFileProvider } from '@services/file'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreMimetypeUtils } from '@services/utils/mimetype'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreUser } from '@features/user/services/user'; | ||||
| import { CoreH5P } from '../services/h5p'; | ||||
| import { CoreH5PCore, CoreH5PDisplayOptions } from './core'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to Moodle's H5P helper class. | ||||
|  */ | ||||
| export class CoreH5PHelper { | ||||
| 
 | ||||
|     /** | ||||
|      * Convert the number representation of display options into an object. | ||||
|      * | ||||
|      * @param displayOptions Number representing display options. | ||||
|      * @return Object with display options. | ||||
|      */ | ||||
|     static decodeDisplayOptions(displayOptions: number): CoreH5PDisplayOptions { | ||||
|         const displayOptionsObject = CoreH5P.instance.h5pCore.getDisplayOptionsAsObject(displayOptions); | ||||
| 
 | ||||
|         const config: CoreH5PDisplayOptions = { | ||||
|             export: false, // Don't allow downloading in the app.
 | ||||
|             embed: false, // Don't display the embed button in the app.
 | ||||
|             copyright: CoreUtils.instance.notNullOrUndefined(displayOptionsObject[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT]) ? | ||||
|                 displayOptionsObject[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT] : false, | ||||
|             icon: CoreUtils.instance.notNullOrUndefined(displayOptionsObject[CoreH5PCore.DISPLAY_OPTION_ABOUT]) ? | ||||
|                 displayOptionsObject[CoreH5PCore.DISPLAY_OPTION_ABOUT] : false, | ||||
|         }; | ||||
| 
 | ||||
|         config.frame = config.copyright || config.export || config.embed; | ||||
| 
 | ||||
|         return config; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the core H5P assets, including all core H5P JavaScript and CSS. | ||||
|      * | ||||
|      * @return Array core H5P assets. | ||||
|      */ | ||||
|     static async getCoreAssets( | ||||
|         siteId?: string, | ||||
|     ): Promise<{settings: CoreH5PCoreSettings; cssRequires: string[]; jsRequires: string[]}> { | ||||
| 
 | ||||
|         // Get core settings.
 | ||||
|         const settings = await CoreH5PHelper.getCoreSettings(siteId); | ||||
| 
 | ||||
|         settings.core = { | ||||
|             styles: [], | ||||
|             scripts: [], | ||||
|         }; | ||||
|         settings.loadedJs = []; | ||||
|         settings.loadedCss = []; | ||||
| 
 | ||||
|         const libUrl = CoreH5P.instance.h5pCore.h5pFS.getCoreH5PPath(); | ||||
|         const cssRequires: string[] = []; | ||||
|         const jsRequires: string[] = []; | ||||
| 
 | ||||
|         // Add core stylesheets.
 | ||||
|         CoreH5PCore.STYLES.forEach((style) => { | ||||
|             settings.core!.styles.push(libUrl + style); | ||||
|             cssRequires.push(libUrl + style); | ||||
|         }); | ||||
| 
 | ||||
|         // Add core JavaScript.
 | ||||
|         CoreH5PCore.getScripts().forEach((script) => { | ||||
|             settings.core!.scripts.push(script); | ||||
|             jsRequires.push(script); | ||||
|         }); | ||||
| 
 | ||||
|         return { settings, cssRequires, jsRequires }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the settings needed by the H5P library. | ||||
|      * | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the settings. | ||||
|      */ | ||||
|     static async getCoreSettings(siteId?: string): Promise<CoreH5PCoreSettings> { | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const userId = site.getUserId(); | ||||
|         const user = await CoreUtils.instance.ignoreErrors(CoreUser.instance.getProfile(userId, undefined, false, siteId)); | ||||
| 
 | ||||
|         if (!user || !user.email) { | ||||
|             throw new CoreError(Translate.instance.instant('core.h5p.errorgetemail')); | ||||
|         } | ||||
| 
 | ||||
|         const basePath = CoreFile.instance.getBasePathInstant(); | ||||
|         const ajaxPaths = { | ||||
|             xAPIResult: '', | ||||
|             contentUserData: '', | ||||
|         }; | ||||
| 
 | ||||
|         return { | ||||
|             baseUrl: CoreFile.instance.getWWWPath(), | ||||
|             url: CoreFile.instance.convertFileSrc( | ||||
|                 CoreTextUtils.instance.concatenatePaths( | ||||
|                     basePath, | ||||
|                     CoreH5P.instance.h5pCore.h5pFS.getExternalH5PFolderPath(site.getId()), | ||||
|                 ), | ||||
|             ), | ||||
|             urlLibraries: CoreFile.instance.convertFileSrc( | ||||
|                 CoreTextUtils.instance.concatenatePaths( | ||||
|                     basePath, | ||||
|                     CoreH5P.instance.h5pCore.h5pFS.getLibrariesFolderPath(site.getId()), | ||||
|                 ), | ||||
|             ), | ||||
|             postUserStatistics: false, | ||||
|             ajax: ajaxPaths, | ||||
|             saveFreq: false, | ||||
|             siteUrl: site.getURL(), | ||||
|             l10n: { | ||||
|                 H5P: CoreH5P.instance.h5pCore.getLocalization(), // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|             }, | ||||
|             user: { name: site.getInfo()!.fullname, mail: user.email }, | ||||
|             hubIsEnabled: false, | ||||
|             reportingIsEnabled: false, | ||||
|             crossorigin: null, | ||||
|             libraryConfig: null, | ||||
|             pluginCacheBuster: '', | ||||
|             libraryUrl: CoreTextUtils.instance.concatenatePaths(CoreH5P.instance.h5pCore.h5pFS.getCoreH5PPath(), 'js'), | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Extract and store an H5P file. | ||||
|      * This function won't validate most things because it should've been done by the server already. | ||||
|      * | ||||
|      * @param fileUrl The file URL used to download the file. | ||||
|      * @param file The file entry of the downloaded file. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param onProgress Function to call on progress. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     static async saveH5P(fileUrl: string, file: FileEntry, siteId?: string, onProgress?: CoreH5PSaveOnProgress): Promise<void> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Notify that the unzip is starting.
 | ||||
|         onProgress && onProgress({ message: 'core.unzipping' }); | ||||
| 
 | ||||
|         const queueId = siteId + ':saveH5P:' + fileUrl; | ||||
| 
 | ||||
|         await CoreH5P.instance.queueRunner.run(queueId, () => CoreH5PHelper.performSave(fileUrl, file, siteId, onProgress)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Extract and store an H5P file. | ||||
|      * | ||||
|      * @param fileUrl The file URL used to download the file. | ||||
|      * @param file The file entry of the downloaded file. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param onProgress Function to call on progress. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected static async performSave( | ||||
|         fileUrl: string, | ||||
|         file: FileEntry, | ||||
|         siteId?: string, | ||||
|         onProgress?: CoreH5PSaveOnProgress, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         const folderName = CoreMimetypeUtils.instance.removeExtension(file.name); | ||||
|         const destFolder = CoreTextUtils.instance.concatenatePaths(CoreFileProvider.TMPFOLDER, 'h5p/' + folderName); | ||||
| 
 | ||||
|         // Unzip the file.
 | ||||
|         await CoreFile.instance.unzipFile(file.toURL(), destFolder, onProgress); | ||||
| 
 | ||||
|         try { | ||||
|             // Notify that the unzip is starting.
 | ||||
|             onProgress && onProgress({ message: 'core.storingfiles' }); | ||||
| 
 | ||||
|             // Read the contents of the unzipped dir, process them and store them.
 | ||||
|             const contents = await CoreFile.instance.getDirectoryContents(destFolder); | ||||
| 
 | ||||
|             const filesData = await CoreH5P.instance.h5pValidator.processH5PFiles(destFolder, contents); | ||||
| 
 | ||||
|             const content = await CoreH5P.instance.h5pStorage.savePackage(filesData, folderName, fileUrl, false, siteId); | ||||
| 
 | ||||
|             // Create the content player.
 | ||||
|             const contentData = await CoreH5P.instance.h5pCore.loadContent(content.id, undefined, siteId); | ||||
| 
 | ||||
|             const embedType = CoreH5PCore.determineEmbedType(contentData.embedType, contentData.library.embedTypes); | ||||
| 
 | ||||
|             await CoreH5P.instance.h5pPlayer.createContentIndex(content.id!, fileUrl, contentData, embedType, siteId); | ||||
|         } finally { | ||||
|             // Remove tmp folder.
 | ||||
|             try { | ||||
|                 await CoreFile.instance.removeDir(destFolder); | ||||
|             } catch (error) { | ||||
|                 // Ignore errors, it will be deleted eventually.
 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Core settings for H5P. | ||||
|  */ | ||||
| export type CoreH5PCoreSettings = { | ||||
|     baseUrl: string; | ||||
|     url: string; | ||||
|     urlLibraries: string; | ||||
|     postUserStatistics: boolean; | ||||
|     ajax: { | ||||
|         xAPIResult: string; | ||||
|         contentUserData: string; | ||||
|     }; | ||||
|     saveFreq: boolean; | ||||
|     siteUrl: string; | ||||
|     l10n: { | ||||
|         H5P: {[name: string]: string}; // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|     }; | ||||
|     user: { | ||||
|         name: string; | ||||
|         mail: string; | ||||
|     }; | ||||
|     hubIsEnabled: boolean; | ||||
|     reportingIsEnabled: boolean; | ||||
|     crossorigin: null; | ||||
|     libraryConfig: null; | ||||
|     pluginCacheBuster: string; | ||||
|     libraryUrl: string; | ||||
|     core?: { | ||||
|         styles: string[]; | ||||
|         scripts: string[]; | ||||
|     }; | ||||
|     loadedJs?: string[]; | ||||
|     loadedCss?: string[]; | ||||
| }; | ||||
| 
 | ||||
| export type CoreH5PSaveOnProgress = (event?: ProgressEvent | { message: string }) => void; | ||||
							
								
								
									
										41
									
								
								src/core/features/h5p/classes/metadata.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/core/features/h5p/classes/metadata.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreH5PLibraryMetadataSettings } from './validator'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to H5P's H5PMetadata class. | ||||
|  */ | ||||
| export class CoreH5PMetadata { | ||||
| 
 | ||||
|     /** | ||||
|      * The metadataSettings field in libraryJson uses 1 for true and 0 for false. | ||||
|      * Here we are converting these to booleans, and also doing JSON encoding. | ||||
|      * | ||||
|      * @param metadataSettings Settings. | ||||
|      * @return Stringified settings. | ||||
|      */ | ||||
|     static boolifyAndEncodeSettings(metadataSettings: CoreH5PLibraryMetadataSettings): string { | ||||
|         // Convert metadataSettings values to boolean.
 | ||||
|         if (typeof metadataSettings.disable != 'undefined') { | ||||
|             metadataSettings.disable = metadataSettings.disable === 1; | ||||
|         } | ||||
|         if (typeof metadataSettings.disableExtraTitleField != 'undefined') { | ||||
|             metadataSettings.disableExtraTitleField = metadataSettings.disableExtraTitleField === 1; | ||||
|         } | ||||
| 
 | ||||
|         return JSON.stringify(metadataSettings); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										420
									
								
								src/core/features/h5p/classes/player.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										420
									
								
								src/core/features/h5p/classes/player.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,420 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreFile } from '@services/file'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUrlUtils } from '@services/utils/url'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreXAPI } from '@features/xapi/services/xapi'; | ||||
| import { CoreH5P } from '../services/h5p'; | ||||
| import { CoreH5PCore, CoreH5PDisplayOptions, CoreH5PContentData, CoreH5PDependenciesFiles } from './core'; | ||||
| import { CoreH5PCoreSettings, CoreH5PHelper } from './helper'; | ||||
| import { CoreH5PStorage } from './storage'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to Moodle's H5P player class. | ||||
|  */ | ||||
| export class CoreH5PPlayer { | ||||
| 
 | ||||
|     constructor( | ||||
|         protected h5pCore: CoreH5PCore, | ||||
|         protected h5pStorage: CoreH5PStorage, | ||||
|     ) { } | ||||
| 
 | ||||
|     /** | ||||
|      * Calculate the URL to the site H5P player. | ||||
|      * | ||||
|      * @param siteUrl Site URL. | ||||
|      * @param fileUrl File URL. | ||||
|      * @param displayOptions Display options. | ||||
|      * @param component Component to send xAPI events to. | ||||
|      * @return URL. | ||||
|      */ | ||||
|     calculateOnlinePlayerUrl(siteUrl: string, fileUrl: string, displayOptions?: CoreH5PDisplayOptions, component?: string): string { | ||||
|         fileUrl = CoreH5P.instance.treatH5PUrl(fileUrl, siteUrl); | ||||
| 
 | ||||
|         const params = this.getUrlParamsFromDisplayOptions(displayOptions); | ||||
|         params.url = encodeURIComponent(fileUrl); | ||||
|         if (component) { | ||||
|             params.component = component; | ||||
|         } | ||||
| 
 | ||||
|         return CoreUrlUtils.instance.addParamsToUrl(CoreTextUtils.instance.concatenatePaths(siteUrl, '/h5p/embed.php'), params); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create the index.html to render an H5P package. | ||||
|      * Part of the code of this function is equivalent to Moodle's add_assets_to_page function. | ||||
|      * | ||||
|      * @param id Content ID. | ||||
|      * @param h5pUrl The URL of the H5P file. | ||||
|      * @param content Content data. | ||||
|      * @param embedType Embed type. The app will always use 'iframe'. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the URL of the index file. | ||||
|      */ | ||||
|     async createContentIndex( | ||||
|         id: number, | ||||
|         h5pUrl: string, | ||||
|         content: CoreH5PContentData, | ||||
|         embedType: string, | ||||
|         siteId?: string, | ||||
|     ): Promise<string> { | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const contentId = this.getContentId(id); | ||||
|         const basePath = CoreFile.instance.getBasePathInstant(); | ||||
|         const contentUrl = CoreFile.instance.convertFileSrc( | ||||
|             CoreTextUtils.instance.concatenatePaths( | ||||
|                 basePath, | ||||
|                 this.h5pCore.h5pFS.getContentFolderPath(content.folderName, site.getId()), | ||||
|             ), | ||||
|         ); | ||||
| 
 | ||||
|         // Create the settings needed for the content.
 | ||||
|         const contentSettings = { | ||||
|             library: CoreH5PCore.libraryToString(content.library), | ||||
|             fullScreen: content.library.fullscreen, | ||||
|             exportUrl: '', // We'll never display the download button, so we don't need the exportUrl.
 | ||||
|             embedCode: this.getEmbedCode(site.getURL(), h5pUrl, true), | ||||
|             resizeCode: this.getResizeCode(), | ||||
|             title: content.slug, | ||||
|             displayOptions: {}, | ||||
|             url: '', // It will be filled using dynamic params if needed.
 | ||||
|             contentUrl: contentUrl, | ||||
|             metadata: content.metadata, | ||||
|             contentUserData: [ | ||||
|                 { | ||||
|                     state: '{}', | ||||
|                 }, | ||||
|             ], | ||||
|         }; | ||||
| 
 | ||||
|         // Get the core H5P assets, needed by the H5P classes to render the H5P content.
 | ||||
|         const result = await this.getAssets(id, content, embedType, site.getId()); | ||||
| 
 | ||||
|         result.settings.contents[contentId] = Object.assign(result.settings.contents[contentId], contentSettings); | ||||
| 
 | ||||
|         const indexPath = this.h5pCore.h5pFS.getContentIndexPath(content.folderName, site.getId()); | ||||
|         let html = '<html><head><title>' + content.title + '</title>' + | ||||
|                 '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'; | ||||
| 
 | ||||
|         // Include the required CSS.
 | ||||
|         result.cssRequires.forEach((cssUrl) => { | ||||
|             html += '<link rel="stylesheet" type="text/css" href="' + cssUrl + '">'; | ||||
|         }); | ||||
| 
 | ||||
|         // Add the settings.
 | ||||
|         html += '<script type="text/javascript">var H5PIntegration = ' + | ||||
|                 JSON.stringify(result.settings).replace(/\//g, '\\/') + '</script>'; | ||||
| 
 | ||||
|         // Add our own script to handle the params.
 | ||||
|         html += '<script type="text/javascript" src="' + CoreTextUtils.instance.concatenatePaths( | ||||
|             this.h5pCore.h5pFS.getCoreH5PPath(), | ||||
|             'moodle/js/params.js', | ||||
|         ) + '"></script>'; | ||||
| 
 | ||||
|         html += '</head><body>'; | ||||
| 
 | ||||
|         // Include the required JS at the beginning of the body, like Moodle web does.
 | ||||
|         // Load the embed.js to allow communication with the parent window.
 | ||||
|         html += '<script type="text/javascript" src="' + | ||||
|                 CoreTextUtils.instance.concatenatePaths(this.h5pCore.h5pFS.getCoreH5PPath(), 'moodle/js/embed.js') + '"></script>'; | ||||
| 
 | ||||
|         result.jsRequires.forEach((jsUrl) => { | ||||
|             html += '<script type="text/javascript" src="' + jsUrl + '"></script>'; | ||||
|         }); | ||||
| 
 | ||||
|         html += '<div class="h5p-iframe-wrapper">' + | ||||
|                 '<iframe id="h5p-iframe-' + id + '" class="h5p-iframe" data-content-id="' + id + '"' + | ||||
|                     'style="height:1px; min-width: 100%" src="about:blank"></iframe>' + | ||||
|                 '</div></body>'; | ||||
| 
 | ||||
|         const fileEntry = await CoreFile.instance.writeFile(indexPath, html); | ||||
| 
 | ||||
|         return fileEntry.toURL(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete all content indexes of all sites from filesystem. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteAllContentIndexes(): Promise<void> { | ||||
|         const siteIds = await CoreSites.instance.getSitesIds(); | ||||
| 
 | ||||
|         await Promise.all(siteIds.map((siteId) => this.deleteAllContentIndexesForSite(siteId))); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete all content indexes for a certain site from filesystem. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteAllContentIndexesForSite(siteId?: string): Promise<void> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         if (!siteId) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const records = await this.h5pCore.h5pFramework.getAllContentData(siteId); | ||||
| 
 | ||||
|         await Promise.all(records.map(async (record) => { | ||||
|             await CoreUtils.instance.ignoreErrors(this.h5pCore.h5pFS.deleteContentIndex(record.foldername, siteId!)); | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete all package content data. | ||||
|      * | ||||
|      * @param fileUrl File URL. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteContentByUrl(fileUrl: string, siteId?: string): Promise<void> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const data = await this.h5pCore.h5pFramework.getContentDataByUrl(fileUrl, siteId); | ||||
| 
 | ||||
|         await CoreUtils.instance.allPromises([ | ||||
|             this.h5pCore.h5pFramework.deleteContentData(data.id, siteId), | ||||
| 
 | ||||
|             this.h5pCore.h5pFS.deleteContentFolder(data.foldername, siteId), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the assets of a package. | ||||
|      * | ||||
|      * @param id Content id. | ||||
|      * @param content Content data. | ||||
|      * @param embedType Embed type. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the assets. | ||||
|      */ | ||||
|     protected async getAssets( | ||||
|         id: number, | ||||
|         content: CoreH5PContentData, | ||||
|         embedType: string, | ||||
|         siteId?: string, | ||||
|     ): Promise<{settings: AssetsSettings; cssRequires: string[]; jsRequires: string[]}> { | ||||
| 
 | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Get core assets.
 | ||||
|         const coreAssets = await CoreH5PHelper.getCoreAssets(siteId); | ||||
| 
 | ||||
|         const contentId = this.getContentId(id); | ||||
|         const settings = <AssetsSettings> coreAssets.settings; | ||||
|         settings.contents = settings.contents || {}; | ||||
|         settings.contents[contentId] = settings.contents[contentId] || {}; | ||||
| 
 | ||||
|         settings.moodleLibraryPaths = await this.h5pCore.getDependencyRoots(id); | ||||
| 
 | ||||
|         /* The filterParameters function should be called before getting the dependency files because it rebuilds content | ||||
|            dependency cache. */ | ||||
|         settings.contents[contentId].jsonContent = await this.h5pCore.filterParameters(content, siteId); | ||||
| 
 | ||||
|         const files = await this.getDependencyFiles(id, content.folderName, siteId); | ||||
| 
 | ||||
|         // H5P checks the embedType in here, but we'll always use iframe so there's no need to do it.
 | ||||
|         // JavaScripts and stylesheets will be loaded through h5p.js.
 | ||||
|         settings.contents[contentId].scripts = this.h5pCore.getAssetsUrls(files.scripts); | ||||
|         settings.contents[contentId].styles = this.h5pCore.getAssetsUrls(files.styles); | ||||
| 
 | ||||
|         return { | ||||
|             settings: settings, | ||||
|             cssRequires: coreAssets.cssRequires, | ||||
|             jsRequires: coreAssets.jsRequires, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the identifier for the H5P content. This identifier is different than the ID stored in the DB. | ||||
|      * | ||||
|      * @param id Package ID. | ||||
|      * @return Content identifier. | ||||
|      */ | ||||
|     protected getContentId(id: number): string { | ||||
|         return 'cid-' + id; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the content index file. | ||||
|      * | ||||
|      * @param fileUrl URL of the H5P package. | ||||
|      * @param displayOptions Display options. | ||||
|      * @param component Component to send xAPI events to. | ||||
|      * @param contextId Context ID where the H5P is. Required for tracking. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the file URL if exists, rejected otherwise. | ||||
|      */ | ||||
|     async getContentIndexFileUrl( | ||||
|         fileUrl: string, | ||||
|         displayOptions?: CoreH5PDisplayOptions, | ||||
|         component?: string, | ||||
|         contextId?: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<string> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const path = await this.h5pCore.h5pFS.getContentIndexFileUrl(fileUrl, siteId); | ||||
| 
 | ||||
|         // Add display options and component to the URL.
 | ||||
|         const data = await this.h5pCore.h5pFramework.getContentDataByUrl(fileUrl, siteId); | ||||
| 
 | ||||
|         displayOptions = this.h5pCore.fixDisplayOptions(displayOptions || {}, data.id); | ||||
| 
 | ||||
|         const params: Record<string, string> = { | ||||
|             displayOptions: JSON.stringify(displayOptions), | ||||
|             component: component || '', | ||||
|         }; | ||||
| 
 | ||||
|         if (contextId) { | ||||
|             params.trackingUrl = await CoreXAPI.instance.getUrl(contextId, 'activity', siteId); | ||||
|         } | ||||
| 
 | ||||
|         return CoreUrlUtils.instance.addParamsToUrl(path, params); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Finds library dependencies files of a certain package. | ||||
|      * | ||||
|      * @param id Content id. | ||||
|      * @param folderName Name of the folder of the content. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the files. | ||||
|      */ | ||||
|     protected async getDependencyFiles(id: number, folderName: string, siteId?: string): Promise<CoreH5PDependenciesFiles> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const preloadedDeps = await CoreH5P.instance.h5pCore.loadContentDependencies(id, 'preloaded', siteId); | ||||
| 
 | ||||
|         return this.h5pCore.getDependenciesFiles( | ||||
|             preloadedDeps, | ||||
|             folderName, | ||||
|             this.h5pCore.h5pFS.getExternalH5PFolderPath(siteId), | ||||
|             siteId, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get display options from a URL params. | ||||
|      * | ||||
|      * @param params URL params. | ||||
|      * @return Display options as object. | ||||
|      */ | ||||
|     getDisplayOptionsFromUrlParams(params?: {[name: string]: string}): CoreH5PDisplayOptions { | ||||
|         const displayOptions: CoreH5PDisplayOptions = {}; | ||||
| 
 | ||||
|         if (!params) { | ||||
|             return displayOptions; | ||||
|         } | ||||
| 
 | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] = false; // Never allow downloading in the app.
 | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_EMBED] = false; // Never show the embed option in the app.
 | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT] = | ||||
|                 CoreUtils.instance.isTrueOrOne(params[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT]); | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_FRAME] = displayOptions[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] || | ||||
|                 displayOptions[CoreH5PCore.DISPLAY_OPTION_EMBED] || displayOptions[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT]; | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_ABOUT] = | ||||
|                 !!this.h5pCore.h5pFramework.getOption(CoreH5PCore.DISPLAY_OPTION_ABOUT, true); | ||||
| 
 | ||||
|         return displayOptions; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Embed code for settings. | ||||
|      * | ||||
|      * @param siteUrl The site URL. | ||||
|      * @param h5pUrl The URL of the .h5p file. | ||||
|      * @param embedEnabled Whether the option to embed the H5P content is enabled. | ||||
|      * @return The HTML code to reuse this H5P content in a different place. | ||||
|      */ | ||||
|     protected getEmbedCode(siteUrl: string, h5pUrl: string, embedEnabled?: boolean): string { | ||||
|         if (!embedEnabled) { | ||||
|             return ''; | ||||
|         } | ||||
| 
 | ||||
|         return '<iframe src="' + this.getEmbedUrl(siteUrl, h5pUrl) + '" allowfullscreen="allowfullscreen"></iframe>'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the encoded URL for embeding an H5P content. | ||||
|      * | ||||
|      * @param siteUrl The site URL. | ||||
|      * @param h5pUrl The URL of the .h5p file. | ||||
|      * @return The embed URL. | ||||
|      */ | ||||
|     protected getEmbedUrl(siteUrl: string, h5pUrl: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(siteUrl, '/h5p/embed.php') + '?url=' + h5pUrl; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Resizing script for settings. | ||||
|      * | ||||
|      * @return The HTML code with the resize script. | ||||
|      */ | ||||
|     protected getResizeCode(): string { | ||||
|         return '<script src="' + this.getResizerScriptUrl() + '"></script>'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the URL to the resizer script. | ||||
|      * | ||||
|      * @return URL. | ||||
|      */ | ||||
|     getResizerScriptUrl(): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(this.h5pCore.h5pFS.getCoreH5PPath(), 'js/h5p-resizer.js'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get online player URL params from display options. | ||||
|      * | ||||
|      * @param options Display options. | ||||
|      * @return Object with URL params. | ||||
|      */ | ||||
|     getUrlParamsFromDisplayOptions(options?: CoreH5PDisplayOptions): {[name: string]: string} { | ||||
|         const params: {[name: string]: string} = {}; | ||||
| 
 | ||||
|         if (!options) { | ||||
|             return params; | ||||
|         } | ||||
| 
 | ||||
|         params[CoreH5PCore.DISPLAY_OPTION_FRAME] = options[CoreH5PCore.DISPLAY_OPTION_FRAME] ? '1' : '0'; | ||||
|         params[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] = options[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] ? '1' : '0'; | ||||
|         params[CoreH5PCore.DISPLAY_OPTION_EMBED] = options[CoreH5PCore.DISPLAY_OPTION_EMBED] ? '1' : '0'; | ||||
|         params[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT] = options[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT] ? '1' : '0'; | ||||
| 
 | ||||
|         return params; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| type AssetsSettings = CoreH5PCoreSettings & { | ||||
|     contents: { | ||||
|         [contentId: string]: { | ||||
|             jsonContent: string | null; | ||||
|             scripts: string[]; | ||||
|             styles: string[]; | ||||
|         }; | ||||
|     }; | ||||
|     moodleLibraryPaths: { | ||||
|         [libString: string]: string; | ||||
|     }; | ||||
| }; | ||||
							
								
								
									
										233
									
								
								src/core/features/h5p/classes/storage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								src/core/features/h5p/classes/storage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,233 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreFile, CoreFileProvider } from '@services/file'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreH5PCore, CoreH5PLibraryBasicData } from './core'; | ||||
| import { CoreH5PFramework } from './framework'; | ||||
| import { CoreH5PMetadata } from './metadata'; | ||||
| import { | ||||
|     CoreH5PLibrariesJsonData, | ||||
|     CoreH5PLibraryJsonData, | ||||
|     CoreH5PLibraryMetadataSettings, | ||||
|     CoreH5PMainJSONFilesData, | ||||
| } from './validator'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to H5P's H5PStorage class. | ||||
|  */ | ||||
| export class CoreH5PStorage { | ||||
| 
 | ||||
|     constructor( | ||||
|         protected h5pCore: CoreH5PCore, | ||||
|         protected h5pFramework: CoreH5PFramework, | ||||
|     ) { } | ||||
| 
 | ||||
|     /** | ||||
|      * Save libraries. | ||||
|      * | ||||
|      * @param librariesJsonData Data about libraries. | ||||
|      * @param folderName Name of the folder of the H5P package. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async saveLibraries(librariesJsonData: CoreH5PLibrariesJsonData, folderName: string, siteId?: string): Promise<void> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         // First of all, try to create the dir where the libraries are stored. This way we don't have to do it for each lib.
 | ||||
|         await CoreFile.instance.createDir(this.h5pCore.h5pFS.getLibrariesFolderPath(siteId)); | ||||
| 
 | ||||
|         const libraryIds: number[] = []; | ||||
| 
 | ||||
|         // Go through libraries that came with this package.
 | ||||
|         await Promise.all(Object.keys(librariesJsonData).map(async (libString) => { | ||||
|             const libraryData: CoreH5PLibraryBeingSaved = librariesJsonData[libString]; | ||||
| 
 | ||||
|             // Find local library identifier.
 | ||||
|             const dbData = await CoreUtils.instance.ignoreErrors(this.h5pFramework.getLibraryByData(libraryData)); | ||||
| 
 | ||||
|             if (dbData) { | ||||
|                 // Library already installed.
 | ||||
|                 libraryData.libraryId = dbData.id; | ||||
| 
 | ||||
|                 const isNewPatch = await this.h5pFramework.isPatchedLibrary(libraryData, dbData); | ||||
| 
 | ||||
|                 if (!isNewPatch) { | ||||
|                     // Same or older version, no need to save.
 | ||||
|                     libraryData.saveDependencies = false; | ||||
| 
 | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             libraryData.saveDependencies = true; | ||||
| 
 | ||||
|             // Convert metadataSettings values to boolean and json_encode it before saving.
 | ||||
|             libraryData.metadataSettings = libraryData.metadataSettings ? | ||||
|                 CoreH5PMetadata.boolifyAndEncodeSettings(<CoreH5PLibraryMetadataSettings> libraryData.metadataSettings) : undefined; | ||||
| 
 | ||||
|             // Save the library data in DB.
 | ||||
|             await this.h5pFramework.saveLibraryData(libraryData, siteId); | ||||
| 
 | ||||
|             // Now save it in FS.
 | ||||
|             try { | ||||
|                 await this.h5pCore.h5pFS.saveLibrary(libraryData, siteId); | ||||
|             } catch (error) { | ||||
|                 if (libraryData.libraryId) { | ||||
|                     // An error occurred, delete the DB data because the lib FS data has been deleted.
 | ||||
|                     await this.h5pFramework.deleteLibrary(libraryData.libraryId, siteId); | ||||
|                 } | ||||
| 
 | ||||
|                 throw error; | ||||
|             } | ||||
| 
 | ||||
|             if (typeof libraryData.libraryId != 'undefined') { | ||||
|                 const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|                 // Remove all indexes of contents that use this library.
 | ||||
|                 promises.push(this.h5pCore.h5pFS.deleteContentIndexesForLibrary(libraryData.libraryId, siteId)); | ||||
| 
 | ||||
|                 if (this.h5pCore.aggregateAssets) { | ||||
|                     // Remove cached assets that use this library.
 | ||||
|                     const removedEntries = await this.h5pFramework.deleteCachedAssets(libraryData.libraryId, siteId); | ||||
| 
 | ||||
|                     await this.h5pCore.h5pFS.deleteCachedAssets(removedEntries, siteId); | ||||
|                 } | ||||
| 
 | ||||
|                 await CoreUtils.instance.allPromises(promises); | ||||
|             } | ||||
|         })); | ||||
| 
 | ||||
|         // Go through the libraries again to save dependencies.
 | ||||
|         await Promise.all(Object.keys(librariesJsonData).map(async (libString) => { | ||||
|             const libraryData: CoreH5PLibraryBeingSaved = librariesJsonData[libString]; | ||||
| 
 | ||||
|             if (!libraryData.saveDependencies || !libraryData.libraryId) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const libId = libraryData.libraryId; | ||||
| 
 | ||||
|             libraryIds.push(libId); | ||||
| 
 | ||||
|             // Remove any old dependencies.
 | ||||
|             await this.h5pFramework.deleteLibraryDependencies(libId, siteId); | ||||
| 
 | ||||
|             // Insert the different new ones.
 | ||||
|             const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|             if (typeof libraryData.preloadedDependencies != 'undefined') { | ||||
|                 promises.push(this.h5pFramework.saveLibraryDependencies(libId, libraryData.preloadedDependencies, 'preloaded')); | ||||
|             } | ||||
|             if (typeof libraryData.dynamicDependencies != 'undefined') { | ||||
|                 promises.push(this.h5pFramework.saveLibraryDependencies(libId, libraryData.dynamicDependencies, 'dynamic')); | ||||
|             } | ||||
|             if (typeof libraryData.editorDependencies != 'undefined') { | ||||
|                 promises.push(this.h5pFramework.saveLibraryDependencies(libId, libraryData.editorDependencies, 'editor')); | ||||
|             } | ||||
| 
 | ||||
|             await Promise.all(promises); | ||||
|         })); | ||||
| 
 | ||||
|         // Make sure dependencies, parameter filtering and export files get regenerated for content who uses these libraries.
 | ||||
|         if (libraryIds.length) { | ||||
|             await this.h5pFramework.clearFilteredParameters(libraryIds, siteId); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save content data in DB and clear cache. | ||||
|      * | ||||
|      * @param content Content to save. | ||||
|      * @param folderName The name of the folder that contains the H5P. | ||||
|      * @param fileUrl The online URL of the package. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the content data. | ||||
|      */ | ||||
|     async savePackage( | ||||
|         data: CoreH5PMainJSONFilesData, | ||||
|         folderName: string, | ||||
|         fileUrl: string, | ||||
|         skipContent?: boolean, | ||||
|         siteId?: string, | ||||
|     ): Promise<CoreH5PContentBeingSaved> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         if (this.h5pCore.mayUpdateLibraries()) { | ||||
|             // Save the libraries that were processed.
 | ||||
|             await this.saveLibraries(data.librariesJsonData, folderName, siteId); | ||||
|         } | ||||
| 
 | ||||
|         const content: CoreH5PContentBeingSaved = {}; | ||||
| 
 | ||||
|         if (!skipContent) { | ||||
|             // Find main library version.
 | ||||
|             if (data.mainJsonData.preloadedDependencies) { | ||||
|                 const mainLib = data.mainJsonData.preloadedDependencies.find((dependency) => | ||||
|                     dependency.machineName === data.mainJsonData.mainLibrary); | ||||
| 
 | ||||
|                 if (mainLib) { | ||||
|                     const id = await this.h5pFramework.getLibraryIdByData(mainLib); | ||||
| 
 | ||||
|                     content.library = Object.assign(mainLib, { libraryId: id }); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             content.params = JSON.stringify(data.contentJsonData); | ||||
| 
 | ||||
|             // Save the content data in DB.
 | ||||
|             await this.h5pCore.saveContent(content, folderName, fileUrl, siteId); | ||||
| 
 | ||||
|             // Save the content files in their right place in FS.
 | ||||
|             const destFolder = CoreTextUtils.instance.concatenatePaths(CoreFileProvider.TMPFOLDER, 'h5p/' + folderName); | ||||
|             const contentPath = CoreTextUtils.instance.concatenatePaths(destFolder, 'content'); | ||||
| 
 | ||||
|             try { | ||||
|                 await this.h5pCore.h5pFS.saveContent(contentPath, folderName, siteId); | ||||
|             } catch (error) { | ||||
|                 // An error occurred, delete the DB data because the content files have been deleted.
 | ||||
|                 await this.h5pFramework.deleteContentData(content.id!, siteId); | ||||
| 
 | ||||
|                 throw error; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return content; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Library to save. | ||||
|  */ | ||||
| export type CoreH5PLibraryBeingSaved = Omit<CoreH5PLibraryJsonData, 'metadataSettings'> & { | ||||
|     libraryId?: number; // Library ID in the DB.
 | ||||
|     saveDependencies?: boolean; // Whether to save dependencies.
 | ||||
|     metadataSettings?: CoreH5PLibraryMetadataSettings | string; // Encoded metadata settings.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data about a content being saved. | ||||
|  */ | ||||
| export type CoreH5PContentBeingSaved = { | ||||
|     id?: number; | ||||
|     params?: string; | ||||
|     library?: CoreH5PContentLibrary; | ||||
| }; | ||||
| 
 | ||||
| export type CoreH5PContentLibrary = CoreH5PLibraryBasicData & { | ||||
|     libraryId?: number; // Library ID in the DB.
 | ||||
| }; | ||||
							
								
								
									
										328
									
								
								src/core/features/h5p/classes/validator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										328
									
								
								src/core/features/h5p/classes/validator.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,328 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreFile, CoreFileFormat } from '@services/file'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreH5PSemantics } from './content-validator'; | ||||
| import { CoreH5PCore, CoreH5PLibraryBasicData } from './core'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to H5P's H5PValidator class. | ||||
|  */ | ||||
| export class CoreH5PValidator { | ||||
| 
 | ||||
|     /** | ||||
|      * Get library data. | ||||
|      * This function won't validate most things because it should've been done by the server already. | ||||
|      * | ||||
|      * @param libDir Directory where the library files are. | ||||
|      * @param libPath Path to the directory where the library files are. | ||||
|      * @return Promise resolved with library data. | ||||
|      */ | ||||
|     protected async getLibraryData(libDir: DirectoryEntry, libPath: string): Promise<CoreH5PLibraryJsonData> { | ||||
| 
 | ||||
|         // Read the required files.
 | ||||
|         const results = await Promise.all([ | ||||
|             this.readLibraryJsonFile(libPath), | ||||
|             this.readLibrarySemanticsFile(libPath), | ||||
|             this.readLibraryLanguageFiles(libPath), | ||||
|             this.libraryHasIcon(libPath), | ||||
|         ]); | ||||
| 
 | ||||
|         const libraryData: CoreH5PLibraryJsonData = results[0]; | ||||
|         libraryData.semantics = results[1]; | ||||
|         libraryData.language = results[2]; | ||||
|         libraryData.hasIcon = results[3]; | ||||
| 
 | ||||
|         return libraryData; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get library data for all libraries in an H5P package. | ||||
|      * | ||||
|      * @param packagePath The path to the package folder. | ||||
|      * @param entries List of files and directories in the root of the package folder. | ||||
|      * @retun Promise resolved with the libraries data. | ||||
|      */ | ||||
|     protected async getPackageLibrariesData( | ||||
|         packagePath: string, | ||||
|         entries: (DirectoryEntry | FileEntry)[], | ||||
|     ): Promise<CoreH5PLibrariesJsonData> { | ||||
| 
 | ||||
|         const libraries: CoreH5PLibrariesJsonData = {}; | ||||
| 
 | ||||
|         await Promise.all(entries.map(async (entry) => { | ||||
|             if (entry.name[0] == '.' || entry.name[0] == '_' || entry.name == 'content' || entry.isFile) { | ||||
|                 // Skip files, the content folder and any folder starting with a . or _.
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const libDirPath = CoreTextUtils.instance.concatenatePaths(packagePath, entry.name); | ||||
| 
 | ||||
|             const libraryData = await this.getLibraryData(<DirectoryEntry> entry, libDirPath); | ||||
| 
 | ||||
|             libraryData.uploadDirectory = libDirPath; | ||||
|             libraries[CoreH5PCore.libraryToString(libraryData)] = libraryData; | ||||
|         })); | ||||
| 
 | ||||
|         return libraries; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the library has an icon file. | ||||
|      * | ||||
|      * @param libPath Path to the directory where the library files are. | ||||
|      * @return Promise resolved with boolean: whether the library has an icon file. | ||||
|      */ | ||||
|     protected async libraryHasIcon(libPath: string): Promise<boolean> { | ||||
|         const path = CoreTextUtils.instance.concatenatePaths(libPath, 'icon.svg'); | ||||
| 
 | ||||
|         try { | ||||
|             // Check if the file exists.
 | ||||
|             await CoreFile.instance.getFile(path); | ||||
| 
 | ||||
|             return true; | ||||
|         } catch (error) { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Process libraries from an H5P library, getting the required data to save them. | ||||
|      * This code is inspired on the isValidPackage function in Moodle's H5PValidator. | ||||
|      * This function won't validate most things because it should've been done by the server already. | ||||
|      * | ||||
|      * @param packagePath The path to the package folder. | ||||
|      * @param entries List of files and directories in the root of the package folder. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async processH5PFiles(packagePath: string, entries: (DirectoryEntry | FileEntry)[]): Promise<CoreH5PMainJSONFilesData> { | ||||
| 
 | ||||
|         // Read the needed files.
 | ||||
|         const results = await Promise.all([ | ||||
|             this.readH5PJsonFile(packagePath), | ||||
|             this.readH5PContentJsonFile(packagePath), | ||||
|             this.getPackageLibrariesData(packagePath, entries), | ||||
|         ]); | ||||
| 
 | ||||
|         return { | ||||
|             librariesJsonData: results[2], | ||||
|             mainJsonData: results[0], | ||||
|             contentJsonData: results[1], | ||||
|         }; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read content.json file and return its parsed contents. | ||||
|      * | ||||
|      * @param packagePath The path to the package folder. | ||||
|      * @return Promise resolved with the parsed file contents. | ||||
|      */ | ||||
|     protected readH5PContentJsonFile(packagePath: string): Promise<unknown> { | ||||
|         const path = CoreTextUtils.instance.concatenatePaths(packagePath, 'content/content.json'); | ||||
| 
 | ||||
|         return CoreFile.instance.readFile(path, CoreFileFormat.FORMATJSON); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read h5p.json file and return its parsed contents. | ||||
|      * | ||||
|      * @param packagePath The path to the package folder. | ||||
|      * @return Promise resolved with the parsed file contents. | ||||
|      */ | ||||
|     protected readH5PJsonFile(packagePath: string): Promise<CoreH5PMainJSONData> { | ||||
|         const path = CoreTextUtils.instance.concatenatePaths(packagePath, 'h5p.json'); | ||||
| 
 | ||||
|         return CoreFile.instance.readFile(path, CoreFileFormat.FORMATJSON); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read library.json file and return its parsed contents. | ||||
|      * | ||||
|      * @param libPath Path to the directory where the library files are. | ||||
|      * @return Promise resolved with the parsed file contents. | ||||
|      */ | ||||
|     protected readLibraryJsonFile(libPath: string): Promise<CoreH5PLibraryMainJsonData> { | ||||
|         const path = CoreTextUtils.instance.concatenatePaths(libPath, 'library.json'); | ||||
| 
 | ||||
|         return CoreFile.instance.readFile<CoreH5PLibraryMainJsonData>(path, CoreFileFormat.FORMATJSON); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read all language files and return their contents indexed by language code. | ||||
|      * | ||||
|      * @param libPath Path to the directory where the library files are. | ||||
|      * @return Promise resolved with the language data. | ||||
|      */ | ||||
|     protected async readLibraryLanguageFiles(libPath: string): Promise<CoreH5PLibraryLangsJsonData | undefined> { | ||||
|         try { | ||||
|             const path = CoreTextUtils.instance.concatenatePaths(libPath, 'language'); | ||||
|             const langIndex: CoreH5PLibraryLangsJsonData = {}; | ||||
| 
 | ||||
|             // Read all the files in the language directory.
 | ||||
|             const entries = await CoreFile.instance.getDirectoryContents(path); | ||||
| 
 | ||||
|             await Promise.all(entries.map(async (entry) => { | ||||
|                 const langFilePath = CoreTextUtils.instance.concatenatePaths(path, entry.name); | ||||
| 
 | ||||
|                 try { | ||||
|                     const langFileData = await CoreFile.instance.readFile<CoreH5PLibraryLangJsonData>( | ||||
|                         langFilePath, | ||||
|                         CoreFileFormat.FORMATJSON, | ||||
|                     ); | ||||
| 
 | ||||
|                     const parts = entry.name.split('.'); // The language code is in parts[0].
 | ||||
|                     langIndex[parts[0]] = langFileData; | ||||
|                 } catch (error) { | ||||
|                     // Ignore this language.
 | ||||
|                 } | ||||
|             })); | ||||
| 
 | ||||
|             return langIndex; | ||||
| 
 | ||||
|         } catch (error) { | ||||
|             // Probably doesn't exist, ignore.
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read semantics.json file and return its parsed contents. | ||||
|      * | ||||
|      * @param libPath Path to the directory where the library files are. | ||||
|      * @return Promise resolved with the parsed file contents. | ||||
|      */ | ||||
|     protected async readLibrarySemanticsFile(libPath: string): Promise<CoreH5PSemantics[] | undefined> { | ||||
|         try { | ||||
|             const path = CoreTextUtils.instance.concatenatePaths(libPath, 'semantics.json'); | ||||
| 
 | ||||
|             return await CoreFile.instance.readFile<CoreH5PSemantics[]>(path, CoreFileFormat.FORMATJSON); | ||||
|         } catch (error) { | ||||
|             // Probably doesn't exist, ignore.
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Data of the main JSON H5P files. | ||||
|  */ | ||||
| export type CoreH5PMainJSONFilesData = { | ||||
|     contentJsonData: unknown; // Contents of content.json file.
 | ||||
|     librariesJsonData: CoreH5PLibrariesJsonData; // JSON data about each library.
 | ||||
|     mainJsonData: CoreH5PMainJSONData; // Contents of h5p.json file.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data stored in h5p.json file of a content. More info in https://h5p.org/documentation/developers/json-file-definitions
 | ||||
|  */ | ||||
| export type CoreH5PMainJSONData = { | ||||
|     title: string; // Title of the content.
 | ||||
|     mainLibrary: string; // The main H5P library for this content.
 | ||||
|     language: string; // Language code.
 | ||||
|     preloadedDependencies?: CoreH5PLibraryBasicData[]; // Dependencies.
 | ||||
|     embedTypes?: ('div' | 'iframe')[]; // List of possible ways to embed the package in the page.
 | ||||
|     authors?: { // The name and role of the content authors
 | ||||
|         name: string; | ||||
|         role: string; | ||||
|     }[]; | ||||
|     source?: string; // The source (a URL) of the licensed material.
 | ||||
|     license?: string; // A code for the content license.
 | ||||
|     licenseVersion?: string; // The version of the license above as a string.
 | ||||
|     licenseExtras?: string; // Any additional information about the license.
 | ||||
|     yearFrom?: string; // If a license is valid for a certain period of time, this represents the start year (as a string).
 | ||||
|     yearTo?: string; // If a license is valid for a certain period of time, this represents the end year (as a string).
 | ||||
|     changes?: { // The changelog.
 | ||||
|         date: string; | ||||
|         author: string; | ||||
|         log: string; | ||||
|     }[]; | ||||
|     authorComments?: string; // Comments for the editor of the content.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * All JSON data for libraries of a package. | ||||
|  */ | ||||
| export type CoreH5PLibrariesJsonData = {[libString: string]: CoreH5PLibraryJsonData}; | ||||
| 
 | ||||
| /** | ||||
|  * All JSON data for a library, including semantics and language. | ||||
|  */ | ||||
| export type CoreH5PLibraryJsonData = CoreH5PLibraryMainJsonData & { | ||||
|     semantics?: CoreH5PSemantics[]; // Data in semantics.json.
 | ||||
|     language?: CoreH5PLibraryLangsJsonData; // Language JSON data.
 | ||||
|     hasIcon?: boolean; // Whether the library has an icon.
 | ||||
|     uploadDirectory?: string; // Path where the lib is stored.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data stored in library.json file of a library. More info in https://h5p.org/library-definition
 | ||||
|  */ | ||||
| export type CoreH5PLibraryMainJsonData = { | ||||
|     title: string; // The human readable name of this library.
 | ||||
|     machineName: string; // The library machine name.
 | ||||
|     majorVersion: number; // Major version.
 | ||||
|     minorVersion: number; // Minor version.
 | ||||
|     patchVersion: number; // Patch version.
 | ||||
|     runnable: number; // Whether or not this library is runnable.
 | ||||
|     coreApi?: { // Required version of H5P Core API.
 | ||||
|         majorVersion: number; | ||||
|         minorVersion: number; | ||||
|     }; | ||||
|     author?: string; // The name of the library author.
 | ||||
|     license?: string; // A code for the content license.
 | ||||
|     description?: string; // Textual description of the library.
 | ||||
|     preloadedDependencies?: CoreH5PLibraryBasicData[]; // Dependencies.
 | ||||
|     dynamicDependencies?: CoreH5PLibraryBasicData[]; // Dependencies.
 | ||||
|     editorDependencies?: CoreH5PLibraryBasicData[]; // Dependencies.
 | ||||
|     preloadedJs?: { path: string }[]; // List of path to the javascript files required for the library.
 | ||||
|     preloadedCss?: { path: string }[]; // List of path to the CSS files to be loaded with the library.
 | ||||
|     embedTypes?: ('div' | 'iframe')[]; // List of possible ways to embed the package in the page.
 | ||||
|     fullscreen?: number; // Enables the integrated full-screen button.
 | ||||
|     metadataSettings?: CoreH5PLibraryMetadataSettings; // Metadata settings.
 | ||||
|     addTo?: CoreH5PLibraryAddTo; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Library metadata settings. | ||||
|  */ | ||||
| export type CoreH5PLibraryMetadataSettings = { | ||||
|     disable?: boolean | number; | ||||
|     disableExtraTitleField?: boolean | number; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Library plugin configuration data. | ||||
|  */ | ||||
| export type CoreH5PLibraryAddTo = { | ||||
|     content?: { | ||||
|         types?: { | ||||
|             text?: { | ||||
|                 regex?: string; | ||||
|             }; | ||||
|         }[]; | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data stored in all languages JSON file of a library. | ||||
|  */ | ||||
| export type CoreH5PLibraryLangsJsonData = {[code: string]: CoreH5PLibraryLangJsonData}; | ||||
| 
 | ||||
| /** | ||||
|  * Data stored in each language JSON file of a library. | ||||
|  */ | ||||
| export type CoreH5PLibraryLangJsonData = { | ||||
|     semantics?: CoreH5PSemantics[]; | ||||
| }; | ||||
							
								
								
									
										51
									
								
								src/core/features/h5p/h5p.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/core/features/h5p/h5p.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| 
 | ||||
| import { CORE_SITE_SCHEMAS } from '@services/sites'; | ||||
| import { | ||||
|     CONTENT_TABLE_NAME, | ||||
|     LIBRARIES_TABLE_NAME, | ||||
|     LIBRARY_DEPENDENCIES_TABLE_NAME, | ||||
|     CONTENTS_LIBRARIES_TABLE_NAME, | ||||
|     LIBRARIES_CACHEDASSETS_TABLE_NAME, | ||||
| } from './services/database/h5p'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|     ], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: CORE_SITE_SCHEMAS, | ||||
|             useValue: [ | ||||
|                 CONTENT_TABLE_NAME, | ||||
|                 LIBRARIES_TABLE_NAME, | ||||
|                 LIBRARY_DEPENDENCIES_TABLE_NAME, | ||||
|                 CONTENTS_LIBRARIES_TABLE_NAME, | ||||
|                 LIBRARIES_CACHEDASSETS_TABLE_NAME, | ||||
|             ], | ||||
|             multi: true, | ||||
|         }, | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             deps: [], | ||||
|             useFactory: () => () => { | ||||
|                 // @todo
 | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
| }) | ||||
| export class CoreH5PModule {} | ||||
							
								
								
									
										308
									
								
								src/core/features/h5p/services/database/h5p.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										308
									
								
								src/core/features/h5p/services/database/h5p.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,308 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreSiteSchema } from '@services/sites'; | ||||
| 
 | ||||
| /** | ||||
|  * Database variables for CoreH5PProvider service. | ||||
|  */ | ||||
| // DB table names.
 | ||||
| export const CONTENT_TABLE_NAME = 'h5p_content'; // H5P content.
 | ||||
| export const LIBRARIES_TABLE_NAME = 'h5p_libraries'; // Installed libraries.
 | ||||
| export const LIBRARY_DEPENDENCIES_TABLE_NAME = 'h5p_library_dependencies'; // Library dependencies.
 | ||||
| export const CONTENTS_LIBRARIES_TABLE_NAME = 'h5p_contents_libraries'; // Which library is used in which content.
 | ||||
| export const LIBRARIES_CACHEDASSETS_TABLE_NAME = 'h5p_libraries_cachedassets'; // H5P cached library assets.
 | ||||
| export const SITE_SCHEMA: CoreSiteSchema = { | ||||
|     name: 'CoreH5PProvider', | ||||
|     version: 1, | ||||
|     canBeCleared: [ | ||||
|         CONTENT_TABLE_NAME, | ||||
|         LIBRARIES_TABLE_NAME, | ||||
|         LIBRARY_DEPENDENCIES_TABLE_NAME, | ||||
|         CONTENTS_LIBRARIES_TABLE_NAME, | ||||
|         LIBRARIES_CACHEDASSETS_TABLE_NAME, | ||||
|     ], | ||||
|     tables: [ | ||||
|         { | ||||
|             name: CONTENT_TABLE_NAME, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'id', | ||||
|                     type: 'INTEGER', | ||||
|                     primaryKey: true, | ||||
|                     autoIncrement: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'jsoncontent', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'mainlibraryid', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'foldername', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'fileurl', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'filtered', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timecreated', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timemodified', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|         { | ||||
|             name: LIBRARIES_TABLE_NAME, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'id', | ||||
|                     type: 'INTEGER', | ||||
|                     primaryKey: true, | ||||
|                     autoIncrement: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'machinename', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'title', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'majorversion', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'minorversion', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'patchversion', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'runnable', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'fullscreen', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'embedtypes', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'preloadedjs', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'preloadedcss', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'droplibrarycss', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'semantics', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'addto', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|         { | ||||
|             name: LIBRARY_DEPENDENCIES_TABLE_NAME, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'id', | ||||
|                     type: 'INTEGER', | ||||
|                     primaryKey: true, | ||||
|                     autoIncrement: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'libraryid', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'requiredlibraryid', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'dependencytype', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|         { | ||||
|             name: CONTENTS_LIBRARIES_TABLE_NAME, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'id', | ||||
|                     type: 'INTEGER', | ||||
|                     primaryKey: true, | ||||
|                     autoIncrement: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'h5pid', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'libraryid', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'dependencytype', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'dropcss', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'weight', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|         { | ||||
|             name: LIBRARIES_CACHEDASSETS_TABLE_NAME, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'id', | ||||
|                     type: 'INTEGER', | ||||
|                     primaryKey: true, | ||||
|                     autoIncrement: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'libraryid', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'hash', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'foldername', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|     ], | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Structure of content data stored in DB. | ||||
|  */ | ||||
| export type CoreH5PContentDBRecord = { | ||||
|     id: number; // The id of the content.
 | ||||
|     jsoncontent: string; // The content in json format.
 | ||||
|     mainlibraryid: number; // The library we first instantiate for this node.
 | ||||
|     foldername: string; // Name of the folder that contains the contents.
 | ||||
|     fileurl: string; // The online URL of the H5P package.
 | ||||
|     filtered: string | null; // Filtered version of json_content.
 | ||||
|     timecreated: number; // Time created.
 | ||||
|     timemodified: number; // Time modified.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Structure of library data stored in DB. | ||||
|  */ | ||||
| export type CoreH5PLibraryDBRecord = { | ||||
|     id: number; // The id of the library.
 | ||||
|     machinename: string; // The library machine name.
 | ||||
|     title: string; // The human readable name of this library.
 | ||||
|     majorversion: number; // Major version.
 | ||||
|     minorversion: number; // Minor version.
 | ||||
|     patchversion: number; // Patch version.
 | ||||
|     runnable: number; // Can this library be started by the module? I.e. not a dependency.
 | ||||
|     fullscreen: number; // Display fullscreen button.
 | ||||
|     embedtypes: string; // List of supported embed types.
 | ||||
|     preloadedjs?: string | null; // Comma separated list of scripts to load.
 | ||||
|     preloadedcss?: string | null; // Comma separated list of stylesheets to load.
 | ||||
|     droplibrarycss?: string | null; // Libraries that should not have CSS included if this lib is used. Comma separated list.
 | ||||
|     semantics?: string | null; // The semantics definition.
 | ||||
|     addto?: string | null; // Plugin configuration data.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Structure of library dependencies stored in DB. | ||||
|  */ | ||||
| export type CoreH5PLibraryDependencyDBRecord = { | ||||
|     id: number; // Id.
 | ||||
|     libraryid: number; // The id of an H5P library.
 | ||||
|     requiredlibraryid: number; // The dependent library to load.
 | ||||
|     dependencytype: string; // Type: preloaded, dynamic, or editor.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Structure of library used by a content stored in DB. | ||||
|  */ | ||||
| export type CoreH5PContentsLibraryDBRecord = { | ||||
|     id: number; | ||||
|     h5pid: number; | ||||
|     libraryid: number; | ||||
|     dependencytype: string; | ||||
|     dropcss: number; | ||||
|     weight: number; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Structure of library cached assets stored in DB. | ||||
|  */ | ||||
| export type CoreH5PLibraryCachedAssetsDBRecord = { | ||||
|     id: number; // Id.
 | ||||
|     libraryid: number; // The id of an H5P library.
 | ||||
|     hash: string; // The hash to identify the cached asset.
 | ||||
|     foldername: string; // Name of the folder that contains the contents.
 | ||||
| }; | ||||
							
								
								
									
										248
									
								
								src/core/features/h5p/services/h5p.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										248
									
								
								src/core/features/h5p/services/h5p.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,248 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreWSExternalWarning, CoreWSExternalFile } from '@services/ws'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUrlUtils } from '@services/utils/url'; | ||||
| import { CoreQueueRunner } from '@classes/queue-runner'; | ||||
| import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; | ||||
| 
 | ||||
| import { CoreH5PCore } from '../classes/core'; | ||||
| import { CoreH5PFramework } from '../classes/framework'; | ||||
| import { CoreH5PPlayer } from '../classes/player'; | ||||
| import { CoreH5PStorage } from '../classes/storage'; | ||||
| import { CoreH5PValidator } from '../classes/validator'; | ||||
| 
 | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to provide H5P functionalities. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class CoreH5PProvider { | ||||
| 
 | ||||
|     h5pCore: CoreH5PCore; | ||||
|     h5pFramework: CoreH5PFramework; | ||||
|     h5pPlayer: CoreH5PPlayer; | ||||
|     h5pStorage: CoreH5PStorage; | ||||
|     h5pValidator: CoreH5PValidator; | ||||
|     queueRunner: CoreQueueRunner; | ||||
| 
 | ||||
|     protected readonly ROOT_CACHE_KEY = 'CoreH5P:'; | ||||
| 
 | ||||
|     constructor() { | ||||
|         this.queueRunner = new CoreQueueRunner(1); | ||||
| 
 | ||||
|         this.h5pValidator = new CoreH5PValidator(); | ||||
|         this.h5pFramework = new CoreH5PFramework(); | ||||
|         this.h5pCore = new CoreH5PCore(this.h5pFramework); | ||||
|         this.h5pStorage = new CoreH5PStorage(this.h5pCore, this.h5pFramework); | ||||
|         this.h5pPlayer = new CoreH5PPlayer(this.h5pCore, this.h5pStorage); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns whether or not WS to get trusted H5P file is available. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with true if ws is available, false otherwise. | ||||
|      * @since 3.8 | ||||
|      */ | ||||
|     async canGetTrustedH5PFile(siteId?: string): Promise<boolean> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         return this.canGetTrustedH5PFileInSite(site); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns whether or not WS to get trusted H5P file is available in a certain site. | ||||
|      * | ||||
|      * @param site Site. If not defined, current site. | ||||
|      * @return Promise resolved with true if ws is available, false otherwise. | ||||
|      * @since 3.8 | ||||
|      */ | ||||
|     canGetTrustedH5PFileInSite(site?: CoreSite): boolean { | ||||
|         site = site || CoreSites.instance.getCurrentSite(); | ||||
| 
 | ||||
|         return !!(site?.wsAvailable('core_h5p_get_trusted_h5p_file')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a trusted H5P file. | ||||
|      * | ||||
|      * @param url The file URL. | ||||
|      * @param options Options. | ||||
|      * @param ignoreCache Whether to ignore cache. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the file data. | ||||
|      */ | ||||
|     async getTrustedH5PFile( | ||||
|         url: string, | ||||
|         options?: CoreH5PGetTrustedFileOptions, | ||||
|         ignoreCache?: boolean, | ||||
|         siteId?: string, | ||||
|     ): Promise<CoreWSExternalFile> { | ||||
| 
 | ||||
|         options = options || {}; | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const data: CoreH5pGetTrustedH5pFileWSParams = { | ||||
|             url: this.treatH5PUrl(url, site.getURL()), | ||||
|             frame: options.frame ? 1 : 0, | ||||
|             export: options.export ? 1 : 0, | ||||
|             embed: options.embed ? 1 : 0, | ||||
|             copyright: options.copyright ? 1 : 0, | ||||
|         }; | ||||
|         const preSets: CoreSiteWSPreSets = { | ||||
|             cacheKey: this.getTrustedH5PFileCacheKey(url), | ||||
|             updateFrequency: CoreSite.FREQUENCY_RARELY, | ||||
|         }; | ||||
| 
 | ||||
|         if (ignoreCache) { | ||||
|             preSets.getFromCache = false; | ||||
|             preSets.emergencyCache = false; | ||||
|         } | ||||
| 
 | ||||
|         const result: CoreH5PGetTrustedH5PFileResult = await site.read('core_h5p_get_trusted_h5p_file', data, preSets); | ||||
| 
 | ||||
|         if (result.warnings && result.warnings.length) { | ||||
|             throw result.warnings[0]; | ||||
|         } | ||||
| 
 | ||||
|         if (result.files && result.files.length) { | ||||
|             return result.files[0]; | ||||
|         } | ||||
| 
 | ||||
|         throw new CoreError('File not found'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get cache key for trusted H5P file WS calls. | ||||
|      * | ||||
|      * @param url The file URL. | ||||
|      * @return Cache key. | ||||
|      */ | ||||
|     protected getTrustedH5PFileCacheKey(url: string): string { | ||||
|         return this.getTrustedH5PFilePrefixCacheKey() + url; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get prefixed cache key for trusted H5P file WS calls. | ||||
|      * | ||||
|      * @return Cache key. | ||||
|      */ | ||||
|     protected getTrustedH5PFilePrefixCacheKey(): string { | ||||
|         return this.ROOT_CACHE_KEY + 'trustedH5PFile:'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidates all trusted H5P file WS calls. | ||||
|      * | ||||
|      * @param siteId Site ID (empty for current site). | ||||
|      * @return Promise resolved when the data is invalidated. | ||||
|      */ | ||||
|     async invalidateAllGetTrustedH5PFile(siteId?: string): Promise<void> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         await site.invalidateWsCacheForKeyStartingWith(this.getTrustedH5PFilePrefixCacheKey()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidates get trusted H5P file WS call. | ||||
|      * | ||||
|      * @param url The URL of the file. | ||||
|      * @param siteId Site ID (empty for current site). | ||||
|      * @return Promise resolved when the data is invalidated. | ||||
|      */ | ||||
|     async invalidateGetTrustedH5PFile(url: string, siteId?: string): Promise<void> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         await site.invalidateWsCacheForKey(this.getTrustedH5PFileCacheKey(url)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether H5P offline is disabled. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with boolean: whether is disabled. | ||||
|      */ | ||||
|     async isOfflineDisabled(siteId?: string): Promise<boolean> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         return this.isOfflineDisabledInSite(site); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether H5P offline is disabled. | ||||
|      * | ||||
|      * @param site Site instance. If not defined, current site. | ||||
|      * @return Whether is disabled. | ||||
|      */ | ||||
|     isOfflineDisabledInSite(site?: CoreSite): boolean { | ||||
|         site = site || CoreSites.instance.getCurrentSite(); | ||||
| 
 | ||||
|         return !!(site?.isFeatureDisabled('NoDelegate_H5POffline')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Treat an H5P url before sending it to WS. | ||||
|      * | ||||
|      * @param url H5P file URL. | ||||
|      * @param siteUrl Site URL. | ||||
|      * @return Treated url. | ||||
|      */ | ||||
|     treatH5PUrl(url: string, siteUrl: string): string { | ||||
|         if (url.indexOf(CoreTextUtils.instance.concatenatePaths(siteUrl, '/webservice/pluginfile.php')) === 0) { | ||||
|             url = url.replace('/webservice/pluginfile', '/pluginfile'); | ||||
|         } | ||||
| 
 | ||||
|         return CoreUrlUtils.instance.removeUrlParams(url); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class CoreH5P extends makeSingleton(CoreH5PProvider) {} | ||||
| 
 | ||||
| /** | ||||
|  * Params of core_h5p_get_trusted_h5p_file WS. | ||||
|  */ | ||||
| export type CoreH5pGetTrustedH5pFileWSParams = { | ||||
|     url: string; // H5P file url.
 | ||||
|     frame?: number; // The frame allow to show the bar options below the content.
 | ||||
|     export?: number; // The export allow to download the package.
 | ||||
|     embed?: number; // The embed allow to copy the code to your site.
 | ||||
|     copyright?: number; // The copyright option.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Options for core_h5p_get_trusted_h5p_file. | ||||
|  */ | ||||
| export type CoreH5PGetTrustedFileOptions = { | ||||
|     frame?: boolean; // Whether to show the bar options below the content.
 | ||||
|     export?: boolean; // Whether to allow to download the package.
 | ||||
|     embed?: boolean; // Whether to allow to copy the code to your site.
 | ||||
|     copyright?: boolean; // The copyright option.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Result of core_h5p_get_trusted_h5p_file. | ||||
|  */ | ||||
| export type CoreH5PGetTrustedH5PFileResult = { | ||||
|     files: CoreWSExternalFile[]; // Files.
 | ||||
|     warnings: CoreWSExternalWarning[]; // List of warnings.
 | ||||
| }; | ||||
| @ -70,10 +70,25 @@ export const enum CoreFileFormat { | ||||
| export class CoreFileProvider { | ||||
| 
 | ||||
|     // Formats to read a file.
 | ||||
|     /** | ||||
|      * @deprecated since 3.9.5, use CoreFileFormat directly. | ||||
|      */ | ||||
|     static readonly FORMATTEXT = CoreFileFormat.FORMATTEXT; | ||||
|     /** | ||||
|      * @deprecated since 3.9.5, use CoreFileFormat directly. | ||||
|      */ | ||||
|     static readonly FORMATDATAURL = CoreFileFormat.FORMATDATAURL; | ||||
|     /** | ||||
|      * @deprecated since 3.9.5, use CoreFileFormat directly. | ||||
|      */ | ||||
|     static readonly FORMATBINARYSTRING = CoreFileFormat.FORMATBINARYSTRING; | ||||
|     /** | ||||
|      * @deprecated since 3.9.5, use CoreFileFormat directly. | ||||
|      */ | ||||
|     static readonly FORMATARRAYBUFFER = CoreFileFormat.FORMATARRAYBUFFER; | ||||
|     /** | ||||
|      * @deprecated since 3.9.5, use CoreFileFormat directly. | ||||
|      */ | ||||
|     static readonly FORMATJSON = CoreFileFormat.FORMATJSON; | ||||
| 
 | ||||
|     // Folders.
 | ||||
| @ -460,19 +475,25 @@ export class CoreFileProvider { | ||||
|      * @param format Format to read the file. | ||||
|      * @return Promise to be resolved when the file is read. | ||||
|      */ | ||||
|     readFile(path: string, format: CoreFileFormat = CoreFileProvider.FORMATTEXT): Promise<string | ArrayBuffer | unknown> { | ||||
|     readFile( | ||||
|         path: string, | ||||
|         format?: CoreFileFormat.FORMATTEXT | CoreFileFormat.FORMATDATAURL | CoreFileFormat.FORMATBINARYSTRING, | ||||
|     ): Promise<string>; | ||||
|     readFile(path: string, format: CoreFileFormat.FORMATARRAYBUFFER): Promise<ArrayBuffer>; | ||||
|     readFile<T = unknown>(path: string, format: CoreFileFormat.FORMATJSON): Promise<T>; | ||||
|     readFile(path: string, format: CoreFileFormat = CoreFileFormat.FORMATTEXT): Promise<string | ArrayBuffer | unknown> { | ||||
|         // Remove basePath if it's in the path.
 | ||||
|         path = this.removeStartingSlash(path.replace(this.basePath, '')); | ||||
|         this.logger.debug('Read file ' + path + ' with format ' + format); | ||||
| 
 | ||||
|         switch (format) { | ||||
|             case CoreFileProvider.FORMATDATAURL: | ||||
|             case CoreFileFormat.FORMATDATAURL: | ||||
|                 return File.instance.readAsDataURL(this.basePath, path); | ||||
|             case CoreFileProvider.FORMATBINARYSTRING: | ||||
|             case CoreFileFormat.FORMATBINARYSTRING: | ||||
|                 return File.instance.readAsBinaryString(this.basePath, path); | ||||
|             case CoreFileProvider.FORMATARRAYBUFFER: | ||||
|             case CoreFileFormat.FORMATARRAYBUFFER: | ||||
|                 return File.instance.readAsArrayBuffer(this.basePath, path); | ||||
|             case CoreFileProvider.FORMATJSON: | ||||
|             case CoreFileFormat.FORMATJSON: | ||||
|                 return File.instance.readAsText(this.basePath, path).then((text) => { | ||||
|                     const parsed = CoreTextUtils.instance.parseJSON(text, null); | ||||
| 
 | ||||
| @ -494,8 +515,8 @@ export class CoreFileProvider { | ||||
|      * @param format Format to read the file. | ||||
|      * @return Promise to be resolved when the file is read. | ||||
|      */ | ||||
|     readFileData(fileData: IFile, format: CoreFileFormat = CoreFileProvider.FORMATTEXT): Promise<string | ArrayBuffer | unknown> { | ||||
|         format = format || CoreFileProvider.FORMATTEXT; | ||||
|     readFileData(fileData: IFile, format: CoreFileFormat = CoreFileFormat.FORMATTEXT): Promise<string | ArrayBuffer | unknown> { | ||||
|         format = format || CoreFileFormat.FORMATTEXT; | ||||
|         this.logger.debug('Read file from file data with format ' + format); | ||||
| 
 | ||||
|         return new Promise((resolve, reject): void => { | ||||
| @ -503,7 +524,7 @@ export class CoreFileProvider { | ||||
| 
 | ||||
|             reader.onloadend = (event): void => { | ||||
|                 if (event.target?.result !== undefined && event.target.result !== null) { | ||||
|                     if (format == CoreFileProvider.FORMATJSON) { | ||||
|                     if (format == CoreFileFormat.FORMATJSON) { | ||||
|                         // Convert to object.
 | ||||
|                         const parsed = CoreTextUtils.instance.parseJSON(<string> event.target.result, null); | ||||
| 
 | ||||
| @ -535,13 +556,13 @@ export class CoreFileProvider { | ||||
|             }, 3000); | ||||
| 
 | ||||
|             switch (format) { | ||||
|                 case CoreFileProvider.FORMATDATAURL: | ||||
|                 case CoreFileFormat.FORMATDATAURL: | ||||
|                     reader.readAsDataURL(fileData); | ||||
|                     break; | ||||
|                 case CoreFileProvider.FORMATBINARYSTRING: | ||||
|                 case CoreFileFormat.FORMATBINARYSTRING: | ||||
|                     reader.readAsBinaryString(fileData); | ||||
|                     break; | ||||
|                 case CoreFileProvider.FORMATARRAYBUFFER: | ||||
|                 case CoreFileFormat.FORMATARRAYBUFFER: | ||||
|                     reader.readAsArrayBuffer(fileData); | ||||
|                     break; | ||||
|                 default: | ||||
|  | ||||
| @ -23,7 +23,7 @@ import { timeout } from 'rxjs/operators'; | ||||
| 
 | ||||
| import { CoreNativeToAngularHttpResponse } from '@classes/native-to-angular-http'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreFile, CoreFileProvider } from '@services/file'; | ||||
| import { CoreFile, CoreFileFormat } from '@services/file'; | ||||
| import { CoreMimetypeUtils } from '@services/utils/mimetype'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUtils, PromiseDefer } from '@services/utils/utils'; | ||||
| @ -855,9 +855,9 @@ export class CoreWSProvider { | ||||
|             // Use the cordova plugin.
 | ||||
|             if (url.indexOf('file://') === 0) { | ||||
|                 // We cannot load local files using the http native plugin. Use file provider instead.
 | ||||
|                 const format = options.responseType == 'json' ? CoreFileProvider.FORMATJSON : CoreFileProvider.FORMATTEXT; | ||||
| 
 | ||||
|                 const content = await CoreFile.instance.readFile(url, format); | ||||
|                 const content = options.responseType == 'json' ? | ||||
|                     await CoreFile.instance.readFile<T>(url, CoreFileFormat.FORMATJSON) : | ||||
|                     await CoreFile.instance.readFile(url, CoreFileFormat.FORMATTEXT); | ||||
| 
 | ||||
|                 return new HttpResponse<T>({ | ||||
|                     body: <T> content, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user