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 = { | export type SQLiteDBRecordValues = { | ||||||
|     [key in string ]: SQLiteDBRecordValue | undefined; |     [key in string ]: SQLiteDBRecordValue | undefined | null; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export type SQLiteDBQueryParams = { | export type SQLiteDBQueryParams = { | ||||||
|  | |||||||
| @ -18,6 +18,7 @@ import { CoreCourseModule } from './course/course.module'; | |||||||
| import { CoreCoursesModule } from './courses/courses.module'; | import { CoreCoursesModule } from './courses/courses.module'; | ||||||
| import { CoreEmulatorModule } from './emulator/emulator.module'; | import { CoreEmulatorModule } from './emulator/emulator.module'; | ||||||
| import { CoreFileUploaderModule } from './fileuploader/fileuploader.module'; | import { CoreFileUploaderModule } from './fileuploader/fileuploader.module'; | ||||||
|  | import { CoreH5PModule } from './h5p/h5p.module'; | ||||||
| import { CoreLoginModule } from './login/login.module'; | import { CoreLoginModule } from './login/login.module'; | ||||||
| import { CoreMainMenuModule } from './mainmenu/mainmenu.module'; | import { CoreMainMenuModule } from './mainmenu/mainmenu.module'; | ||||||
| import { CoreSettingsModule } from './settings/settings.module'; | import { CoreSettingsModule } from './settings/settings.module'; | ||||||
| @ -41,6 +42,7 @@ import { CoreXAPIModule } from './xapi/xapi.module'; | |||||||
|         CoreUserModule, |         CoreUserModule, | ||||||
|         CorePushNotificationsModule, |         CorePushNotificationsModule, | ||||||
|         CoreXAPIModule, |         CoreXAPIModule, | ||||||
|  |         CoreH5PModule, | ||||||
|     ], |     ], | ||||||
| }) | }) | ||||||
| export class CoreFeaturesModule {} | 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 { | export class CoreFileProvider { | ||||||
| 
 | 
 | ||||||
|     // Formats to read a file.
 |     // Formats to read a file.
 | ||||||
|  |     /** | ||||||
|  |      * @deprecated since 3.9.5, use CoreFileFormat directly. | ||||||
|  |      */ | ||||||
|     static readonly FORMATTEXT = CoreFileFormat.FORMATTEXT; |     static readonly FORMATTEXT = CoreFileFormat.FORMATTEXT; | ||||||
|  |     /** | ||||||
|  |      * @deprecated since 3.9.5, use CoreFileFormat directly. | ||||||
|  |      */ | ||||||
|     static readonly FORMATDATAURL = CoreFileFormat.FORMATDATAURL; |     static readonly FORMATDATAURL = CoreFileFormat.FORMATDATAURL; | ||||||
|  |     /** | ||||||
|  |      * @deprecated since 3.9.5, use CoreFileFormat directly. | ||||||
|  |      */ | ||||||
|     static readonly FORMATBINARYSTRING = CoreFileFormat.FORMATBINARYSTRING; |     static readonly FORMATBINARYSTRING = CoreFileFormat.FORMATBINARYSTRING; | ||||||
|  |     /** | ||||||
|  |      * @deprecated since 3.9.5, use CoreFileFormat directly. | ||||||
|  |      */ | ||||||
|     static readonly FORMATARRAYBUFFER = CoreFileFormat.FORMATARRAYBUFFER; |     static readonly FORMATARRAYBUFFER = CoreFileFormat.FORMATARRAYBUFFER; | ||||||
|  |     /** | ||||||
|  |      * @deprecated since 3.9.5, use CoreFileFormat directly. | ||||||
|  |      */ | ||||||
|     static readonly FORMATJSON = CoreFileFormat.FORMATJSON; |     static readonly FORMATJSON = CoreFileFormat.FORMATJSON; | ||||||
| 
 | 
 | ||||||
|     // Folders.
 |     // Folders.
 | ||||||
| @ -460,19 +475,25 @@ export class CoreFileProvider { | |||||||
|      * @param format Format to read the file. |      * @param format Format to read the file. | ||||||
|      * @return Promise to be resolved when the file is read. |      * @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.
 |         // Remove basePath if it's in the path.
 | ||||||
|         path = this.removeStartingSlash(path.replace(this.basePath, '')); |         path = this.removeStartingSlash(path.replace(this.basePath, '')); | ||||||
|         this.logger.debug('Read file ' + path + ' with format ' + format); |         this.logger.debug('Read file ' + path + ' with format ' + format); | ||||||
| 
 | 
 | ||||||
|         switch (format) { |         switch (format) { | ||||||
|             case CoreFileProvider.FORMATDATAURL: |             case CoreFileFormat.FORMATDATAURL: | ||||||
|                 return File.instance.readAsDataURL(this.basePath, path); |                 return File.instance.readAsDataURL(this.basePath, path); | ||||||
|             case CoreFileProvider.FORMATBINARYSTRING: |             case CoreFileFormat.FORMATBINARYSTRING: | ||||||
|                 return File.instance.readAsBinaryString(this.basePath, path); |                 return File.instance.readAsBinaryString(this.basePath, path); | ||||||
|             case CoreFileProvider.FORMATARRAYBUFFER: |             case CoreFileFormat.FORMATARRAYBUFFER: | ||||||
|                 return File.instance.readAsArrayBuffer(this.basePath, path); |                 return File.instance.readAsArrayBuffer(this.basePath, path); | ||||||
|             case CoreFileProvider.FORMATJSON: |             case CoreFileFormat.FORMATJSON: | ||||||
|                 return File.instance.readAsText(this.basePath, path).then((text) => { |                 return File.instance.readAsText(this.basePath, path).then((text) => { | ||||||
|                     const parsed = CoreTextUtils.instance.parseJSON(text, null); |                     const parsed = CoreTextUtils.instance.parseJSON(text, null); | ||||||
| 
 | 
 | ||||||
| @ -494,8 +515,8 @@ export class CoreFileProvider { | |||||||
|      * @param format Format to read the file. |      * @param format Format to read the file. | ||||||
|      * @return Promise to be resolved when the file is read. |      * @return Promise to be resolved when the file is read. | ||||||
|      */ |      */ | ||||||
|     readFileData(fileData: IFile, format: CoreFileFormat = CoreFileProvider.FORMATTEXT): Promise<string | ArrayBuffer | unknown> { |     readFileData(fileData: IFile, format: CoreFileFormat = CoreFileFormat.FORMATTEXT): Promise<string | ArrayBuffer | unknown> { | ||||||
|         format = format || CoreFileProvider.FORMATTEXT; |         format = format || CoreFileFormat.FORMATTEXT; | ||||||
|         this.logger.debug('Read file from file data with format ' + format); |         this.logger.debug('Read file from file data with format ' + format); | ||||||
| 
 | 
 | ||||||
|         return new Promise((resolve, reject): void => { |         return new Promise((resolve, reject): void => { | ||||||
| @ -503,7 +524,7 @@ export class CoreFileProvider { | |||||||
| 
 | 
 | ||||||
|             reader.onloadend = (event): void => { |             reader.onloadend = (event): void => { | ||||||
|                 if (event.target?.result !== undefined && event.target.result !== null) { |                 if (event.target?.result !== undefined && event.target.result !== null) { | ||||||
|                     if (format == CoreFileProvider.FORMATJSON) { |                     if (format == CoreFileFormat.FORMATJSON) { | ||||||
|                         // Convert to object.
 |                         // Convert to object.
 | ||||||
|                         const parsed = CoreTextUtils.instance.parseJSON(<string> event.target.result, null); |                         const parsed = CoreTextUtils.instance.parseJSON(<string> event.target.result, null); | ||||||
| 
 | 
 | ||||||
| @ -535,13 +556,13 @@ export class CoreFileProvider { | |||||||
|             }, 3000); |             }, 3000); | ||||||
| 
 | 
 | ||||||
|             switch (format) { |             switch (format) { | ||||||
|                 case CoreFileProvider.FORMATDATAURL: |                 case CoreFileFormat.FORMATDATAURL: | ||||||
|                     reader.readAsDataURL(fileData); |                     reader.readAsDataURL(fileData); | ||||||
|                     break; |                     break; | ||||||
|                 case CoreFileProvider.FORMATBINARYSTRING: |                 case CoreFileFormat.FORMATBINARYSTRING: | ||||||
|                     reader.readAsBinaryString(fileData); |                     reader.readAsBinaryString(fileData); | ||||||
|                     break; |                     break; | ||||||
|                 case CoreFileProvider.FORMATARRAYBUFFER: |                 case CoreFileFormat.FORMATARRAYBUFFER: | ||||||
|                     reader.readAsArrayBuffer(fileData); |                     reader.readAsArrayBuffer(fileData); | ||||||
|                     break; |                     break; | ||||||
|                 default: |                 default: | ||||||
|  | |||||||
| @ -23,7 +23,7 @@ import { timeout } from 'rxjs/operators'; | |||||||
| 
 | 
 | ||||||
| import { CoreNativeToAngularHttpResponse } from '@classes/native-to-angular-http'; | import { CoreNativeToAngularHttpResponse } from '@classes/native-to-angular-http'; | ||||||
| import { CoreApp } from '@services/app'; | import { CoreApp } from '@services/app'; | ||||||
| import { CoreFile, CoreFileProvider } from '@services/file'; | import { CoreFile, CoreFileFormat } from '@services/file'; | ||||||
| import { CoreMimetypeUtils } from '@services/utils/mimetype'; | import { CoreMimetypeUtils } from '@services/utils/mimetype'; | ||||||
| import { CoreTextUtils } from '@services/utils/text'; | import { CoreTextUtils } from '@services/utils/text'; | ||||||
| import { CoreUtils, PromiseDefer } from '@services/utils/utils'; | import { CoreUtils, PromiseDefer } from '@services/utils/utils'; | ||||||
| @ -855,9 +855,9 @@ export class CoreWSProvider { | |||||||
|             // Use the cordova plugin.
 |             // Use the cordova plugin.
 | ||||||
|             if (url.indexOf('file://') === 0) { |             if (url.indexOf('file://') === 0) { | ||||||
|                 // We cannot load local files using the http native plugin. Use file provider instead.
 |                 // 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 = options.responseType == 'json' ? | ||||||
| 
 |                     await CoreFile.instance.readFile<T>(url, CoreFileFormat.FORMATJSON) : | ||||||
|                 const content = await CoreFile.instance.readFile(url, format); |                     await CoreFile.instance.readFile(url, CoreFileFormat.FORMATTEXT); | ||||||
| 
 | 
 | ||||||
|                 return new HttpResponse<T>({ |                 return new HttpResponse<T>({ | ||||||
|                     body: <T> content, |                     body: <T> content, | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user