MOBILE-4670 h5p: Use online player if package has missing dependencies
parent
dc7bea0b2e
commit
4cbb3fe7f0
|
@ -1571,6 +1571,7 @@
|
|||
"core.confirmopeninbrowser": "local_moodlemobileapp",
|
||||
"core.confirmremoveselectedfile": "local_moodlemobileapp",
|
||||
"core.confirmremoveselectedfiles": "local_moodlemobileapp",
|
||||
"core.connectandtryagain": "local_moodlemobileapp",
|
||||
"core.connectionlost": "local_moodlemobileapp",
|
||||
"core.considereddigitalminor": "moodle",
|
||||
"core.contactsupport": "local_moodlemobileapp",
|
||||
|
@ -1588,11 +1589,14 @@
|
|||
"core.copytoclipboard": "local_moodlemobileapp",
|
||||
"core.course": "moodle",
|
||||
"core.course.activitydisabled": "local_moodlemobileapp",
|
||||
"core.course.activitynotavailableoffline": "local_moodlemobileapp",
|
||||
"core.course.activitynotyetviewableremoteaddon": "local_moodlemobileapp",
|
||||
"core.course.activityrequiresconnection": "local_moodlemobileapp",
|
||||
"core.course.allsections": "local_moodlemobileapp",
|
||||
"core.course.aria:sectionprogress": "local_moodlemobileapp",
|
||||
"core.course.availablespace": "local_moodlemobileapp",
|
||||
"core.course.cannotdeletewhiledownloading": "local_moodlemobileapp",
|
||||
"core.course.changesofflinemaybelost": "local_moodlemobileapp",
|
||||
"core.course.communicationroomlink": "course",
|
||||
"core.course.completion_automatic:done": "course",
|
||||
"core.course.completion_automatic:failed": "course",
|
||||
|
@ -2269,6 +2273,7 @@
|
|||
"core.mygroups": "group",
|
||||
"core.name": "moodle",
|
||||
"core.needhelp": "local_moodlemobileapp",
|
||||
"core.needinternettoaccessit": "local_moodlemobileapp",
|
||||
"core.networkerroriframemsg": "local_moodlemobileapp",
|
||||
"core.networkerrormsg": "local_moodlemobileapp",
|
||||
"core.never": "moodle",
|
||||
|
|
|
@ -27,15 +27,21 @@
|
|||
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"
|
||||
[courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()" />
|
||||
|
||||
<!-- Offline disabled. -->
|
||||
<ion-card class="core-warning-card" *ngIf="!siteCanDownload && playing">
|
||||
<!-- User tried to play in offline a package that must be played in online. -->
|
||||
@if (triedToPlay && !isOnline && (!siteCanDownload || hasMissingDependencies)) {
|
||||
<ion-card class="core-warning-card">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true" />
|
||||
<ion-label>
|
||||
@if (!siteCanDownload) {
|
||||
{{ 'core.h5p.offlinedisabled' | translate }} {{ 'addon.mod_h5pactivity.offlinedisabledwarning' | translate }}
|
||||
} @else {
|
||||
{{ 'core.course.activitynotavailableoffline' | translate }} {{ 'core.needinternettoaccessit' | translate }}
|
||||
}
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
}
|
||||
|
||||
<!-- Preview mode. -->
|
||||
<ion-card class="core-warning-card" *ngIf="accessInfo && !trackComponent">
|
||||
|
@ -69,7 +75,7 @@
|
|||
|
||||
<core-h5p-iframe *ngIf="playing" [fileUrl]="fileUrl" [displayOptions]="displayOptions" [onlinePlayerUrl]="onlinePlayerUrl"
|
||||
[trackComponent]="trackComponent" [contextId]="h5pActivity?.context" [enableInAppFullscreen]="true" [saveFreq]="saveFreq"
|
||||
[state]="contentState" />
|
||||
[state]="contentState" [component]="component" [componentId]="componentId" [fileTimemodified]="deployedFile?.timemodified" />
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModuleId]="module.id" />
|
||||
|
|
|
@ -53,6 +53,11 @@ import {
|
|||
ADDON_MOD_H5PACTIVITY_STATE_ID,
|
||||
ADDON_MOD_H5PACTIVITY_TRACK_COMPONENT,
|
||||
} from '../../constants';
|
||||
import { CoreH5PMissingDependenciesError } from '@features/h5p/classes/errors/missing-dependencies-error';
|
||||
import { CoreToasts, ToastDuration } from '@services/toasts';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { NgZone, Translate } from '@singletons';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
|
||||
/**
|
||||
* Component that displays an H5P activity entry page.
|
||||
|
@ -89,8 +94,11 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
|||
isOpeningPage = false;
|
||||
canViewAllAttempts = false;
|
||||
saveStateEnabled = false;
|
||||
hasMissingDependencies = false;
|
||||
saveFreq?: number;
|
||||
contentState?: string;
|
||||
isOnline: boolean;
|
||||
triedToPlay = false;
|
||||
|
||||
protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity';
|
||||
protected syncEventName = ADDON_MOD_H5PACTIVITY_AUTO_SYNCED;
|
||||
|
@ -98,6 +106,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
|||
protected observer?: CoreEventObserver;
|
||||
protected messageListenerFunction: (event: MessageEvent) => Promise<void>;
|
||||
protected checkCompletionAfterLog = false; // It's called later, when the user plays the package.
|
||||
protected onlineObserver: Subscription;
|
||||
|
||||
constructor(
|
||||
protected content?: IonContent,
|
||||
|
@ -111,6 +120,39 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
|||
// Listen for messages from the iframe.
|
||||
this.messageListenerFunction = (event) => this.onIframeMessage(event);
|
||||
window.addEventListener('message', this.messageListenerFunction);
|
||||
|
||||
this.isOnline = CoreNetwork.isOnline();
|
||||
this.onlineObserver = CoreNetwork.onChange().subscribe(() => {
|
||||
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
||||
NgZone.run(() => {
|
||||
this.networkChanged();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* React to a network status change.
|
||||
*/
|
||||
protected networkChanged(): void {
|
||||
const wasOnline = this.isOnline;
|
||||
this.isOnline = CoreNetwork.isOnline();
|
||||
|
||||
if (this.playing && !this.fileUrl && !this.isOnline && wasOnline && this.trackComponent) {
|
||||
// User lost connection while playing an online package with tracking. Show an error.
|
||||
CoreDomUtils.showErrorModal(new CoreError(Translate.instant('core.course.changesofflinemaybelost'), {
|
||||
title: Translate.instant('core.youreoffline'),
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isOnline && this.triedToPlay) {
|
||||
// User couldn't play the package because he was offline, but he reconnected. Try again.
|
||||
this.triedToPlay = false;
|
||||
this.play();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -164,7 +206,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
|||
);
|
||||
}
|
||||
|
||||
if (!this.siteCanDownload || this.state === DownloadStatus.DOWNLOADED) {
|
||||
if (!this.siteCanDownload || this.state === DownloadStatus.DOWNLOADED || this.hasMissingDependencies) {
|
||||
// Cannot download the file or already downloaded, play the package directly.
|
||||
this.play();
|
||||
|
||||
|
@ -219,12 +261,18 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
|||
return;
|
||||
}
|
||||
|
||||
this.deployedFile = await AddonModH5PActivity.getDeployedFile(this.h5pActivity, {
|
||||
const deployedFile = await AddonModH5PActivity.getDeployedFile(this.h5pActivity, {
|
||||
displayOptions: this.displayOptions,
|
||||
siteId: this.siteId,
|
||||
});
|
||||
|
||||
this.fileUrl = CoreFileHelper.getFileUrl(this.deployedFile);
|
||||
this.hasMissingDependencies = await AddonModH5PActivity.hasMissingDependencies(this.module.id, deployedFile);
|
||||
if (this.hasMissingDependencies) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.deployedFile = deployedFile;
|
||||
this.fileUrl = CoreFileHelper.getFileUrl(deployedFile);
|
||||
|
||||
// Listen for changes in the state.
|
||||
const eventName = await CoreFilepool.getFileEventNameByUrl(this.site.getId(), this.fileUrl);
|
||||
|
@ -362,6 +410,20 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
|||
return;
|
||||
}
|
||||
|
||||
if (error instanceof CoreH5PMissingDependenciesError) {
|
||||
// Cannot be played offline, use online player.
|
||||
this.hasMissingDependencies = true;
|
||||
this.fileUrl = undefined;
|
||||
this.play();
|
||||
|
||||
CoreToasts.show({
|
||||
message: Translate.instant('core.course.activityrequiresconnection'),
|
||||
duration: ToastDuration.LONG,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
|
||||
}
|
||||
}
|
||||
|
@ -448,6 +510,16 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
|||
return;
|
||||
}
|
||||
|
||||
if (!this.fileUrl && !this.isOnline) {
|
||||
this.triedToPlay = true;
|
||||
|
||||
CoreDomUtils.showErrorModal(new CoreError(Translate.instant('core.connectandtryagain'), {
|
||||
title: Translate.instant('core.course.activitynotavailableoffline'),
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.playing = true;
|
||||
|
||||
// Mark the activity as viewed.
|
||||
|
@ -456,6 +528,11 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
|||
this.checkCompletion();
|
||||
|
||||
this.analyticsLogEvent('mod_h5pactivity_view_h5pactivity');
|
||||
|
||||
if (!this.fileUrl && this.trackComponent) {
|
||||
// User is playing the package in online, invalidate attempts to fetch latest data.
|
||||
AddonModH5PActivity.invalidateUserAttempts(this.h5pActivity.id, CoreSites.getCurrentSiteUserId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -466,6 +543,11 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
|||
const userId = CoreSites.getCurrentSiteUserId();
|
||||
|
||||
try {
|
||||
if (!this.fileUrl && this.trackComponent && this.h5pActivity) {
|
||||
// User is playing the package in online, invalidate attempts to fetch latest data.
|
||||
await AddonModH5PActivity.invalidateUserAttempts(this.h5pActivity.id, CoreSites.getCurrentSiteUserId());
|
||||
}
|
||||
|
||||
await CoreNavigator.navigateToSitePath(
|
||||
`${ADDON_MOD_H5PACTIVITY_PAGE_NAME}/${this.courseId}/${this.module.id}/userattempts/${userId}`,
|
||||
);
|
||||
|
@ -732,6 +814,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
|||
super.ngOnDestroy();
|
||||
|
||||
this.observer?.off();
|
||||
this.onlineObserver.unsubscribe();
|
||||
|
||||
// Wait a bit to make sure all messages have been received.
|
||||
setTimeout(() => {
|
||||
|
|
|
@ -32,6 +32,9 @@ import {
|
|||
AddonModH5PActivityGradeMethod,
|
||||
} from '../constants';
|
||||
import { CoreCacheUpdateFrequency } from '@/core/constants';
|
||||
import { CoreFileHelper } from '@services/file-helper';
|
||||
import { CorePromiseUtils } from '@singletons/promise-utils';
|
||||
import { CoreH5PMissingDependencyDBRecord } from '@features/h5p/services/database/h5p';
|
||||
|
||||
/**
|
||||
* Service that provides some features for H5P activity.
|
||||
|
@ -571,6 +574,45 @@ export class AddonModH5PActivityProvider {
|
|||
return this.getH5PActivityByField(courseId, 'id', id, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get missing dependencies for a certain H5P activity.
|
||||
*
|
||||
* @param componentId Component ID.
|
||||
* @param deployedFile File to check.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns Missing dependencies, empty if no missing dependencies.
|
||||
*/
|
||||
async getMissingDependencies(
|
||||
componentId: number,
|
||||
deployedFile: CoreWSFile,
|
||||
siteId?: string,
|
||||
): Promise<CoreH5PMissingDependencyDBRecord[]> {
|
||||
const fileUrl = CoreFileHelper.getFileUrl(deployedFile);
|
||||
|
||||
const missingDependencies =
|
||||
await CoreH5P.h5pFramework.getMissingDependenciesForComponent(ADDON_MOD_H5PACTIVITY_COMPONENT, componentId, siteId);
|
||||
if (!missingDependencies.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// The activity had missing dependencies, but the package could have changed (e.g. the teacher fixed it).
|
||||
// Check which of the dependencies apply to the current package.
|
||||
const fileId = await CoreH5P.h5pFramework.getFileIdForMissingDependencies(fileUrl, siteId);
|
||||
|
||||
const filteredMissingDependencies = missingDependencies.filter(dependency =>
|
||||
dependency.fileid === fileId && dependency.filetimemodified === deployedFile.timemodified);
|
||||
if (filteredMissingDependencies.length > 0) {
|
||||
return filteredMissingDependencies;
|
||||
}
|
||||
|
||||
// Package has changed, delete previous missing dependencies.
|
||||
await CorePromiseUtils.ignoreErrors(
|
||||
CoreH5P.h5pFramework.deleteMissingDependenciesForComponent(ADDON_MOD_H5PACTIVITY_COMPONENT, componentId, siteId),
|
||||
);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for attemps WS calls.
|
||||
*
|
||||
|
@ -658,6 +700,20 @@ export class AddonModH5PActivityProvider {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a package has missing dependencies.
|
||||
*
|
||||
* @param componentId Component ID.
|
||||
* @param deployedFile File to check.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns Whether the package has missing dependencies.
|
||||
*/
|
||||
async hasMissingDependencies(componentId: number, deployedFile: CoreWSFile, siteId?: string): Promise<boolean> {
|
||||
const missingDependencies = await this.getMissingDependencies(componentId, deployedFile, siteId);
|
||||
|
||||
return missingDependencies.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates access information.
|
||||
*
|
||||
|
|
|
@ -144,6 +144,12 @@ export class AddonModH5PActivityPrefetchHandlerService extends CoreCourseActivit
|
|||
siteId: siteId,
|
||||
});
|
||||
|
||||
// If we already detected that the file has missing dependencies there's no need to download it again.
|
||||
const missingDependencies = await AddonModH5PActivity.getMissingDependencies(module.id, deployedFile, siteId);
|
||||
if (missingDependencies.length > 0) {
|
||||
throw CoreH5P.h5pFramework.buildMissingDependenciesErrorFromDBRecords(missingDependencies);
|
||||
}
|
||||
|
||||
if (AddonModH5PActivity.isSaveStateEnabled(h5pActivity)) {
|
||||
// If the file needs to be downloaded, delete the states because it means the package has changed or user deleted it.
|
||||
const fileState = await CoreFilepool.getFileStateByUrl(siteId, CoreFileHelper.getFileUrl(deployedFile));
|
||||
|
@ -254,6 +260,19 @@ export class AddonModH5PActivityPrefetchHandlerService extends CoreCourseActivit
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async removeFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<void> {
|
||||
// Remove files and delete any missing dependency stored to force recalculating them.
|
||||
await Promise.all([
|
||||
super.removeFiles(module, courseId),
|
||||
CorePromiseUtils.ignoreErrors(
|
||||
CoreH5P.h5pFramework.deleteMissingDependenciesForComponent(ADDON_MOD_H5PACTIVITY_COMPONENT, module.id),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const AddonModH5PActivityPrefetchHandler = makeSingleton(AddonModH5PActivityPrefetchHandlerService);
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
{
|
||||
"activitydisabled": "Your organisation has disabled this activity in the mobile app.",
|
||||
"activitynotavailableoffline": "This activity is not available offline.",
|
||||
"activitynotyetviewableremoteaddon": "Your organisation installed a plugin that is not yet supported.",
|
||||
"activityrequiresconnection": "This activity is only available with an internet connection. If your device is offline, you will not be able to access it.",
|
||||
"allsections": "All sections",
|
||||
"aria:sectionprogress": "Section progress:",
|
||||
"availablespace": "You currently have about {{available}} free space.",
|
||||
"cannotdeletewhiledownloading": "Files cannot be deleted while the activity is being downloaded. Please wait for the download to finish.",
|
||||
"changesofflinemaybelost": "Any changes you make to this activity while offline may not be saved.<br><br>Connect your device to the internet to avoid losing your progress.",
|
||||
"communicationroomlink": "Chat to course participants",
|
||||
"completion_automatic:done": "Done:",
|
||||
"completion_automatic:failed": "Failed:",
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
// (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 { CoreError } from '@classes/errors/error';
|
||||
import { CoreH5PMissingLibrary } from '../core';
|
||||
|
||||
/**
|
||||
* Missing dependencies error when deploying an H5P package.
|
||||
*/
|
||||
export class CoreH5PMissingDependenciesError extends CoreError {
|
||||
|
||||
missingDependencies: CoreH5PMissingLibrary[];
|
||||
|
||||
constructor(message: string, missingDependencies: CoreH5PMissingLibrary[]) {
|
||||
super(message);
|
||||
|
||||
this.missingDependencies = missingDependencies;
|
||||
}
|
||||
|
||||
}
|
|
@ -24,6 +24,7 @@ import {
|
|||
CoreH5PContentDepsTreeDependency,
|
||||
CoreH5PLibraryBasicData,
|
||||
CoreH5PLibraryBasicDataWithPatch,
|
||||
CoreH5PMissingLibrary,
|
||||
} from './core';
|
||||
import {
|
||||
CONTENT_TABLE_NAME,
|
||||
|
@ -36,6 +37,10 @@ import {
|
|||
CoreH5PLibraryDBRecord,
|
||||
CoreH5PLibraryDependencyDBRecord,
|
||||
CoreH5PContentsLibraryDBRecord,
|
||||
CoreH5PMissingDependencyDBRecord,
|
||||
MISSING_DEPENDENCIES_TABLE_NAME,
|
||||
MISSING_DEPENDENCIES_PRIMARY_KEYS,
|
||||
CoreH5PMissingDependencyDBPrimaryKeys,
|
||||
} from '../services/database/h5p';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CoreH5PSemantics } from './content-validator';
|
||||
|
@ -48,6 +53,11 @@ import { LazyMap, lazyMap } from '@/core/utils/lazy-map';
|
|||
import { CoreDatabaseTable } from '@classes/database/database-table';
|
||||
import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy';
|
||||
import { SubPartial } from '@/core/utils/types';
|
||||
import { CoreH5PMissingDependenciesError } from './errors/missing-dependencies-error';
|
||||
import { CoreFilepool } from '@services/filepool';
|
||||
import { CoreFileHelper } from '@services/file-helper';
|
||||
import { CoreUrl, CoreUrlPartNames } from '@singletons/url';
|
||||
import { CorePromiseUtils } from '@singletons/promise-utils';
|
||||
|
||||
/**
|
||||
* Equivalent to Moodle's implementation of H5PFrameworkInterface.
|
||||
|
@ -59,6 +69,9 @@ export class CoreH5PFramework {
|
|||
protected libraryDependenciesTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreH5PLibraryDependencyDBRecord>>>;
|
||||
protected contentsLibrariesTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreH5PContentsLibraryDBRecord>>>;
|
||||
protected librariesCachedAssetsTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreH5PLibraryCachedAssetsDBRecord>>>;
|
||||
protected missingDependenciesTables: LazyMap<
|
||||
AsyncInstance<CoreDatabaseTable<CoreH5PMissingDependencyDBRecord, CoreH5PMissingDependencyDBPrimaryKeys>>
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
this.contentTables = lazyMap(
|
||||
|
@ -121,6 +134,43 @@ export class CoreH5PFramework {
|
|||
),
|
||||
),
|
||||
);
|
||||
this.missingDependenciesTables = lazyMap(
|
||||
siteId => asyncInstance(
|
||||
() => CoreSites.getSiteTable(
|
||||
MISSING_DEPENDENCIES_TABLE_NAME,
|
||||
{
|
||||
siteId,
|
||||
config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
|
||||
onDestroy: () => delete this.missingDependenciesTables[siteId],
|
||||
primaryKeyColumns: [...MISSING_DEPENDENCIES_PRIMARY_KEYS],
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of missing dependencies DB records, create a missing dependencies error.
|
||||
*
|
||||
* @param missingDependencies List of missing dependencies.
|
||||
* @returns Error instance.
|
||||
*/
|
||||
buildMissingDependenciesErrorFromDBRecords(
|
||||
missingDependencies: CoreH5PMissingDependencyDBRecord[],
|
||||
): CoreH5PMissingDependenciesError {
|
||||
const missingLibraries = missingDependencies.map(dep => ({
|
||||
machineName: dep.machinename,
|
||||
majorVersion: dep.majorversion,
|
||||
minorVersion: dep.minorversion,
|
||||
libString: dep.requiredby,
|
||||
}));
|
||||
|
||||
const errorMessage = Translate.instant('core.h5p.missingdependency', { $a: {
|
||||
lib: missingLibraries[0].libString,
|
||||
dep: CoreH5PCore.libraryToString(missingLibraries[0]),
|
||||
} });
|
||||
|
||||
return new CoreH5PMissingDependenciesError(errorMessage, missingLibraries);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -236,6 +286,33 @@ export class CoreH5PFramework {
|
|||
await this.contentsLibrariesTables[siteId].delete({ h5pid: id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete missing dependencies stored for a certain component and componentId.
|
||||
*
|
||||
* @param component Component.
|
||||
* @param componentId Component ID.
|
||||
* @param siteId Site ID.
|
||||
*/
|
||||
async deleteMissingDependenciesForComponent(component: string, componentId: string | number, siteId?: string): Promise<void> {
|
||||
siteId ??= CoreSites.getCurrentSiteId();
|
||||
|
||||
await this.missingDependenciesTables[siteId].delete({ component, componentId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the missing dependencies related to a certain library version.
|
||||
*
|
||||
* @param libraryData Library.
|
||||
* @param siteId Site ID.
|
||||
*/
|
||||
protected async deleteMissingDependenciesForLibrary(libraryData: CoreH5PLibraryBasicData, siteId: string): Promise<void> {
|
||||
await this.missingDependenciesTables[siteId].delete({
|
||||
machinename: libraryData.machineName,
|
||||
majorversion: libraryData.majorVersion,
|
||||
minorversion: libraryData.minorVersion,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all conent data from DB.
|
||||
*
|
||||
|
@ -282,6 +359,38 @@ export class CoreH5PFramework {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an identifier for a file URL, used to store missing dependencies.
|
||||
*
|
||||
* @param fileUrl File URL.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns An identifier for the file.
|
||||
*/
|
||||
async getFileIdForMissingDependencies(fileUrl: string, siteId?: string): Promise<string> {
|
||||
siteId ??= CoreSites.getCurrentSiteId();
|
||||
|
||||
const isTrusted = await CoreH5P.isTrustedUrl(fileUrl, siteId);
|
||||
if (!isTrusted) {
|
||||
// Fix the URL, we need to URL of the trusted package.
|
||||
const file = await CoreFilepool.fixPluginfileURL(siteId, fileUrl);
|
||||
|
||||
fileUrl = CoreFileHelper.getFileUrl(file);
|
||||
}
|
||||
|
||||
// Remove all params from the URL except the time modified. We don't want the id to depend on changing params like
|
||||
// the language or the token.
|
||||
const urlParams = CoreUrl.extractUrlParams(fileUrl);
|
||||
fileUrl = CoreUrl.addParamsToUrl(
|
||||
CoreUrl.removeUrlParts(fileUrl, [CoreUrlPartNames.Query]),
|
||||
{ modified: urlParams.modified },
|
||||
);
|
||||
|
||||
// Only return the file args, that way the id doesn't depend on the endpoint to obtain the file.
|
||||
const fileArgs = CoreUrl.getPluginFileArgs(fileUrl);
|
||||
|
||||
return fileArgs ? fileArgs.join('/') : fileUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest library version.
|
||||
*
|
||||
|
@ -405,6 +514,47 @@ export class CoreH5PFramework {
|
|||
return this.getLibraryId(libraryData.machineName, libraryData.majorVersion, libraryData.minorVersion, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get missing dependencies stored for a certain component and componentId.
|
||||
*
|
||||
* @param component Component.
|
||||
* @param componentId Component ID.
|
||||
* @param siteId Site ID.
|
||||
* @returns List of missing dependencies. Empty list if no missing dependencies stored for the file.
|
||||
*/
|
||||
async getMissingDependenciesForComponent(
|
||||
component: string,
|
||||
componentId: string | number,
|
||||
siteId?: string,
|
||||
): Promise<CoreH5PMissingDependencyDBRecord[]> {
|
||||
siteId ??= CoreSites.getCurrentSiteId();
|
||||
|
||||
try {
|
||||
return await this.missingDependenciesTables[siteId].getMany({ component, componentId });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get missing dependencies stored for a certain file.
|
||||
*
|
||||
* @param fileUrl File URL.
|
||||
* @param siteId Site ID.
|
||||
* @returns List of missing dependencies. Empty list if no missing dependencies stored for the file.
|
||||
*/
|
||||
async getMissingDependenciesForFile(fileUrl: string, siteId?: string): Promise<CoreH5PMissingDependencyDBRecord[]> {
|
||||
siteId ??= CoreSites.getCurrentSiteId();
|
||||
|
||||
try {
|
||||
const fileId = await this.getFileIdForMissingDependencies(fileUrl, siteId);
|
||||
|
||||
return await this.missingDependenciesTables[siteId].getMany({ fileid: fileId });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default behaviour for the display option defined.
|
||||
*
|
||||
|
@ -812,6 +962,9 @@ export class CoreH5PFramework {
|
|||
// Updated libary. Remove old dependencies.
|
||||
await this.deleteLibraryDependencies(data.id, siteId);
|
||||
}
|
||||
|
||||
// Delete missing dependencies related to this library. Don't block the execution for this.
|
||||
CorePromiseUtils.ignoreErrors(this.deleteMissingDependenciesForLibrary(libraryData, siteId));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -830,6 +983,7 @@ export class CoreH5PFramework {
|
|||
siteId?: string,
|
||||
): Promise<void> {
|
||||
const targetSiteId = siteId ?? CoreSites.getCurrentSiteId();
|
||||
const libString = CoreH5PCore.libraryToString(library);
|
||||
|
||||
await Promise.all(dependencies.map(async (dependency) => {
|
||||
// Get the ID of the library.
|
||||
|
@ -837,10 +991,10 @@ export class CoreH5PFramework {
|
|||
|
||||
if (!dependencyId) {
|
||||
// Missing dependency. It should have been detected before installing the package.
|
||||
throw new CoreError(Translate.instant('core.h5p.missingdependency', { $a: {
|
||||
throw new CoreH5PMissingDependenciesError(Translate.instant('core.h5p.missingdependency', { $a: {
|
||||
lib: CoreH5PCore.libraryToString(library),
|
||||
dep: CoreH5PCore.libraryToString(dependency),
|
||||
} }));
|
||||
} }), [{ ...dependency, libString }]);
|
||||
}
|
||||
|
||||
// Create the relation.
|
||||
|
@ -900,6 +1054,34 @@ export class CoreH5PFramework {
|
|||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store missing dependencies in DB.
|
||||
*
|
||||
* @param fileUrl URL of the package that has missing dependencies.
|
||||
* @param missingDependencies List of missing dependencies.
|
||||
* @param options Other options.
|
||||
*/
|
||||
async storeMissingDependencies(
|
||||
fileUrl: string,
|
||||
missingDependencies: CoreH5PMissingLibrary[],
|
||||
options: StoreMissingDependenciesOptions = {},
|
||||
): Promise<void> {
|
||||
const targetSiteId = options.siteId ?? CoreSites.getCurrentSiteId();
|
||||
|
||||
const fileId = await this.getFileIdForMissingDependencies(fileUrl, targetSiteId);
|
||||
|
||||
await Promise.all(missingDependencies.map((missingLibrary) => this.missingDependenciesTables[targetSiteId].insert({
|
||||
fileid: fileId,
|
||||
machinename: missingLibrary.machineName,
|
||||
majorversion: missingLibrary.majorVersion,
|
||||
minorversion: missingLibrary.minorVersion,
|
||||
requiredby: missingLibrary.libString,
|
||||
filetimemodified: options.fileTimemodified ?? 0,
|
||||
component: options.component,
|
||||
componentId: options.componentId,
|
||||
})));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save content data in DB and clear cache.
|
||||
*
|
||||
|
@ -1011,3 +1193,13 @@ type LibraryDependency = {
|
|||
type LibraryAddonDBData = Omit<CoreH5PLibraryAddonData, 'addTo'> & {
|
||||
addTo: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for storeMissingDependencies.
|
||||
*/
|
||||
type StoreMissingDependenciesOptions = {
|
||||
component?: string;
|
||||
componentId?: string | number;
|
||||
fileTimemodified?: number;
|
||||
siteId?: string;
|
||||
};
|
||||
|
|
|
@ -21,6 +21,9 @@ import { CoreH5P } from '../services/h5p';
|
|||
import { CoreH5PCore, CoreH5PDisplayOptions, CoreH5PLocalization } from './core';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CorePath } from '@singletons/path';
|
||||
import { CorePluginFileTreatDownloadedFileOptions } from '@services/plugin-file-delegate';
|
||||
import { CoreH5PMissingDependenciesError } from './errors/missing-dependencies-error';
|
||||
import { CorePromiseUtils } from '@singletons/promise-utils';
|
||||
|
||||
/**
|
||||
* Equivalent to Moodle's H5P helper class.
|
||||
|
@ -89,13 +92,13 @@ export class CoreH5PHelper {
|
|||
|
||||
// Add core stylesheets.
|
||||
CoreH5PCore.STYLES.forEach((style) => {
|
||||
settings.core!.styles.push(libUrl + style);
|
||||
settings.core?.styles.push(libUrl + style);
|
||||
cssRequires.push(libUrl + style);
|
||||
});
|
||||
|
||||
// Add core JavaScript.
|
||||
CoreH5PCore.getScripts().forEach((script) => {
|
||||
settings.core!.scripts.push(script);
|
||||
settings.core?.scripts.push(script);
|
||||
jsRequires.push(script);
|
||||
});
|
||||
|
||||
|
@ -163,19 +166,22 @@ export class CoreH5PHelper {
|
|||
*
|
||||
* @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.
|
||||
* @param options Options.
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
static async saveH5P(fileUrl: string, file: FileEntry, siteId?: string, onProgress?: CoreH5PSaveOnProgress): Promise<void> {
|
||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||
static async saveH5P(
|
||||
fileUrl: string,
|
||||
file: FileEntry,
|
||||
options: CorePluginFileTreatDownloadedFileOptions<ProgressEvent | { message: string }> = {},
|
||||
): Promise<void> {
|
||||
const siteId = options.siteId || CoreSites.getCurrentSiteId();
|
||||
|
||||
// Notify that the unzip is starting.
|
||||
onProgress && onProgress({ message: 'core.unzipping' });
|
||||
options.onProgress && options.onProgress({ message: 'core.unzipping' });
|
||||
|
||||
const queueId = siteId + ':saveH5P:' + fileUrl;
|
||||
|
||||
await CoreH5P.queueRunner.run(queueId, () => CoreH5PHelper.performSave(fileUrl, file, siteId, onProgress));
|
||||
await CoreH5P.queueRunner.run(queueId, () => CoreH5PHelper.performSave(fileUrl, file, { ...options, siteId }));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -183,40 +189,54 @@ export class CoreH5PHelper {
|
|||
*
|
||||
* @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.
|
||||
* @param options Options.
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
protected static async performSave(
|
||||
fileUrl: string,
|
||||
file: FileEntry,
|
||||
siteId?: string,
|
||||
onProgress?: CoreH5PSaveOnProgress,
|
||||
options: CorePluginFileTreatDownloadedFileOptions<ProgressEvent | { message: string }> = {},
|
||||
): Promise<void> {
|
||||
|
||||
const folderName = CoreMimetypeUtils.removeExtension(file.name);
|
||||
const destFolder = CorePath.concatenatePaths(CoreFileProvider.TMPFOLDER, 'h5p/' + folderName);
|
||||
|
||||
// Unzip the file.
|
||||
await CoreFile.unzipFile(CoreFile.getFileEntryURL(file), destFolder, onProgress);
|
||||
await CoreFile.unzipFile(CoreFile.getFileEntryURL(file), destFolder, options.onProgress);
|
||||
|
||||
try {
|
||||
// Notify that the unzip is starting.
|
||||
onProgress && onProgress({ message: 'core.storingfiles' });
|
||||
options.onProgress && options.onProgress({ message: 'core.storingfiles' });
|
||||
|
||||
// Read the contents of the unzipped dir, process them and store them.
|
||||
const contents = await CoreFile.getDirectoryContents(destFolder);
|
||||
|
||||
const filesData = await CoreH5P.h5pValidator.processH5PFiles(destFolder, contents, siteId);
|
||||
const filesData = await CoreH5P.h5pValidator.processH5PFiles(destFolder, contents, options.siteId);
|
||||
|
||||
const content = await CoreH5P.h5pStorage.savePackage(filesData, folderName, fileUrl, false, siteId);
|
||||
const content = await CoreH5P.h5pStorage.savePackage(filesData, folderName, fileUrl, false, options.siteId);
|
||||
|
||||
// Create the content player.
|
||||
const contentData = await CoreH5P.h5pCore.loadContent(content.id, undefined, siteId);
|
||||
const contentData = await CoreH5P.h5pCore.loadContent(content.id, undefined, options.siteId);
|
||||
|
||||
const embedType = CoreH5PCore.determineEmbedType(contentData.embedType, contentData.library.embedTypes);
|
||||
|
||||
await CoreH5P.h5pPlayer.createContentIndex(content.id!, fileUrl, contentData, embedType, siteId);
|
||||
await CoreH5P.h5pPlayer.createContentIndex(content.id!, fileUrl, contentData, embedType, options.siteId);
|
||||
} catch (error) {
|
||||
if (error instanceof CoreH5PMissingDependenciesError) {
|
||||
// Store the missing dependencies to avoid re-downloading the file every time.
|
||||
await CorePromiseUtils.ignoreErrors(CoreH5P.h5pFramework.storeMissingDependencies(
|
||||
fileUrl,
|
||||
error.missingDependencies,
|
||||
{
|
||||
component: options.component,
|
||||
componentId: options.componentId,
|
||||
fileTimemodified: options.timemodified,
|
||||
siteId: options.siteId,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
// Remove tmp folder.
|
||||
try {
|
||||
|
@ -264,5 +284,3 @@ export type CoreH5PCoreSettings = {
|
|||
loadedJs?: string[];
|
||||
loadedCss?: string[];
|
||||
};
|
||||
|
||||
export type CoreH5PSaveOnProgress = (event?: ProgressEvent | { message: string }) => void;
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CoreH5PMissingDependenciesError } from './errors/missing-dependencies-error';
|
||||
import { FileEntry, DirectoryEntry } from '@awesome-cordova-plugins/file/ngx';
|
||||
import { CoreFile, CoreFileFormat } from '@services/file';
|
||||
import { Translate } from '@singletons';
|
||||
|
@ -269,10 +269,10 @@ export class CoreH5PValidator {
|
|||
const libString = Object.keys(missingLibraries)[0];
|
||||
const missingLibrary = missingLibraries[libString];
|
||||
|
||||
throw new CoreError(Translate.instant('core.h5p.missingdependency', { $a: {
|
||||
throw new CoreH5PMissingDependenciesError(Translate.instant('core.h5p.missingdependency', { $a: {
|
||||
lib: missingLibrary.libString,
|
||||
dep: libString,
|
||||
} }));
|
||||
} }), Object.values(missingLibraries));
|
||||
}
|
||||
|
||||
return { librariesJsonData, mainJsonData, contentJsonData };
|
||||
|
|
|
@ -49,6 +49,9 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy {
|
|||
@Input({ transform: toBoolean }) enableInAppFullscreen = false; // Whether to enable our custom in-app fullscreen feature.
|
||||
@Input() saveFreq?: number; // Save frequency (in seconds) if enabled.
|
||||
@Input() state?: string; // Initial content state.
|
||||
@Input() component?: string; // Component the file is linked to.
|
||||
@Input() componentId?: string | number; // Component ID.
|
||||
@Input() fileTimemodified?: number; // The timemodified of the file.
|
||||
@Output() onIframeUrlSet = new EventEmitter<{src: string; online: boolean}>();
|
||||
@Output() onIframeLoaded = new EventEmitter<void>();
|
||||
|
||||
|
@ -181,7 +184,12 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy {
|
|||
|
||||
const file = await CoreFile.getFile(path);
|
||||
|
||||
await CoreH5PHelper.saveH5P(this.fileUrl!, file, this.siteId);
|
||||
await CoreH5PHelper.saveH5P(this.fileUrl!, file, {
|
||||
siteId: this.siteId,
|
||||
component: this.component,
|
||||
componentId: this.componentId,
|
||||
timemodified: this.fileTimemodified,
|
||||
});
|
||||
|
||||
// File treated. Try to get the index file URL again.
|
||||
const url = await CoreH5P.h5pPlayer.getContentIndexFileUrl(
|
||||
|
|
|
@ -9,4 +9,5 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<core-h5p-iframe *ngIf="showPackage" [fileUrl]="urlParams!.url" [displayOptions]="displayOptions" [onlinePlayerUrl]="src" />
|
||||
<core-h5p-iframe *ngIf="showPackage" [fileUrl]="urlParams!.url" [displayOptions]="displayOptions" [onlinePlayerUrl]="src"
|
||||
[component]="component" [componentId]="componentId" [fileTimemodified]="fileTimemodified" />
|
||||
|
|
|
@ -41,6 +41,7 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
@Input() src?: string; // The URL of the player to display the H5P package.
|
||||
@Input() component?: string; // Component.
|
||||
@Input() componentId?: string | number; // Component ID to use in conjunction with the component.
|
||||
@Input() fileTimemodified?: number; // The timemodified of the package file.
|
||||
|
||||
showPackage = false;
|
||||
state?: string;
|
||||
|
@ -122,6 +123,12 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
|
||||
try {
|
||||
// Check if the package has missing dependencies. If so, it cannot be downloaded.
|
||||
const missingDependencies = await CoreH5P.h5pFramework.getMissingDependenciesForFile(this.urlParams.url);
|
||||
if (missingDependencies.length > 0) {
|
||||
throw CoreH5P.h5pFramework.buildMissingDependenciesErrorFromDBRecords(missingDependencies);
|
||||
}
|
||||
|
||||
// Get the file size and ask the user to confirm.
|
||||
const size = await CorePluginFileDelegate.getFileSize({ fileurl: this.urlParams.url }, this.siteId);
|
||||
|
||||
|
@ -152,6 +159,12 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if the package has missing dependencies. If so, it cannot be downloaded.
|
||||
const missingDependencies = await CoreH5P.h5pFramework.getMissingDependenciesForFile(this.urlParams.url);
|
||||
if (missingDependencies.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the file size.
|
||||
const size = await CorePluginFileDelegate.getFileSize({ fileurl: this.urlParams.url }, this.siteId);
|
||||
|
||||
|
|
|
@ -24,15 +24,19 @@ 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 MISSING_DEPENDENCIES_TABLE_NAME = 'h5p_missing_dependencies'; // Information about missing dependencies.
|
||||
export const MISSING_DEPENDENCIES_PRIMARY_KEYS = ['fileid', 'machinename', 'majorversion', 'minorversion'] as const;
|
||||
|
||||
export const SITE_SCHEMA: CoreSiteSchema = {
|
||||
name: 'CoreH5PProvider',
|
||||
version: 2,
|
||||
version: 3,
|
||||
canBeCleared: [
|
||||
CONTENT_TABLE_NAME,
|
||||
LIBRARIES_TABLE_NAME,
|
||||
LIBRARY_DEPENDENCIES_TABLE_NAME,
|
||||
CONTENTS_LIBRARIES_TABLE_NAME,
|
||||
LIBRARIES_CACHEDASSETS_TABLE_NAME,
|
||||
MISSING_DEPENDENCIES_TABLE_NAME,
|
||||
],
|
||||
tables: [
|
||||
{
|
||||
|
@ -243,6 +247,46 @@ export const SITE_SCHEMA: CoreSiteSchema = {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: MISSING_DEPENDENCIES_TABLE_NAME,
|
||||
columns: [
|
||||
{
|
||||
name: 'fileid',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'machinename',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'majorversion',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'minorversion',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'requiredby',
|
||||
type: 'TEXT',
|
||||
notNull: true,
|
||||
},
|
||||
{
|
||||
name: 'filetimemodified',
|
||||
type: 'INTEGER',
|
||||
notNull: true,
|
||||
},
|
||||
{
|
||||
name: 'component',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'componentId',
|
||||
type: 'TEXT',
|
||||
},
|
||||
],
|
||||
primaryKeys: [...MISSING_DEPENDENCIES_PRIMARY_KEYS],
|
||||
},
|
||||
],
|
||||
async migrate(db: SQLiteDB, oldVersion: number): Promise<void> {
|
||||
if (oldVersion >= 2) {
|
||||
|
@ -320,3 +364,19 @@ export type CoreH5PLibraryCachedAssetsDBRecord = {
|
|||
hash: string; // The hash to identify the cached asset.
|
||||
foldername: string; // Name of the folder that contains the contents.
|
||||
};
|
||||
|
||||
/**
|
||||
* Structure of missing dependency data stored in DB.
|
||||
*/
|
||||
export type CoreH5PMissingDependencyDBRecord = {
|
||||
fileid: string; // Identifier of the package that has a missing dependency. It will be part of the file url.
|
||||
filetimemodified: number; // Time when the file was last modified.
|
||||
machinename: string; // Machine name of the missing dependency.
|
||||
majorversion: number; // Major version of the missing dependency.
|
||||
minorversion: number; // Minor version of the missing dependency.
|
||||
requiredby: string; // LibString of the library that requires the missing dependency.
|
||||
component?: string; // Component related to the package.
|
||||
componentId?: string | number; // Component ID related to the package.
|
||||
};
|
||||
|
||||
export type CoreH5PMissingDependencyDBPrimaryKeys = typeof MISSING_DEPENDENCIES_PRIMARY_KEYS[number];
|
||||
|
|
|
@ -234,6 +234,19 @@ export class CoreH5PProvider {
|
|||
return !!(site?.isOfflineDisabled() || site?.isFeatureDisabled('NoDelegate_H5POffline'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an H5P URL, check if it's a trusted URL.
|
||||
*
|
||||
* @param fileUrl File URL to check.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns Whether it's a trusted URL.
|
||||
*/
|
||||
async isTrustedUrl(fileUrl: string, siteId?: string): Promise<boolean> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
return site.containsUrl(fileUrl) && !!fileUrl.match(/pluginfile\.php\/([^/]+\/)?[^/]+\/core_h5p\/export\//i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Treat an H5P url before sending it to WS.
|
||||
*
|
||||
|
|
|
@ -15,8 +15,11 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { FileEntry } from '@awesome-cordova-plugins/file/ngx';
|
||||
|
||||
import { CoreFilepoolOnProgressCallback } from '@services/filepool';
|
||||
import { CorePluginFileDownloadableResult, CorePluginFileHandler } from '@services/plugin-file-delegate';
|
||||
import {
|
||||
CorePluginFileDownloadableResult,
|
||||
CorePluginFileHandler,
|
||||
CorePluginFileTreatDownloadedFileOptions,
|
||||
} from '@services/plugin-file-delegate';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreMimetypeUtils } from '@services/utils/mimetype';
|
||||
import { CoreUrl } from '@singletons/url';
|
||||
|
@ -36,12 +39,7 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler {
|
|||
name = 'CoreH5PPluginFileHandler';
|
||||
|
||||
/**
|
||||
* React to a file being deleted.
|
||||
*
|
||||
* @param fileUrl The file URL used to download the file.
|
||||
* @param path The path of the deleted file.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns Promise resolved when done.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async fileDeleted(fileUrl: string, path: string, siteId?: string): Promise<void> {
|
||||
// If an h5p file is deleted, remove the contents folder.
|
||||
|
@ -49,18 +47,14 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler {
|
|||
}
|
||||
|
||||
/**
|
||||
* Check whether a file can be downloaded. If so, return the file to download.
|
||||
*
|
||||
* @param file The file data.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns Promise resolved with the file to use. Rejected if cannot download.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getDownloadableFile(file: CoreWSFile, siteId?: string): Promise<CoreWSFile> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||
const fileUrl = CoreFileHelper.getFileUrl(file);
|
||||
|
||||
if (site.containsUrl(fileUrl) && fileUrl.match(/pluginfile\.php\/[^/]+\/core_h5p\/export\//i)) {
|
||||
const isTrusted = await CoreH5P.isTrustedUrl(fileUrl, siteId);
|
||||
if (isTrusted) {
|
||||
// It's already a deployed file, use it.
|
||||
return file;
|
||||
}
|
||||
|
@ -69,11 +63,7 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler {
|
|||
}
|
||||
|
||||
/**
|
||||
* Given an HTML element, get the URLs of the files that should be downloaded and weren't treated by
|
||||
* CoreFilepoolProvider.extractDownloadableFilesFromHtml.
|
||||
*
|
||||
* @param container Container where to get the URLs from.
|
||||
* @returns List of URLs.
|
||||
* @inheritdoc
|
||||
*/
|
||||
getDownloadableFilesFromHTML(container: HTMLElement): string[] {
|
||||
const iframes = <HTMLIFrameElement[]> Array.from(container.querySelectorAll('iframe.h5p-iframe'));
|
||||
|
@ -91,11 +81,7 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get a file size.
|
||||
*
|
||||
* @param file The file data.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns Promise resolved with the size.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getFileSize(file: CoreWSFile, siteId?: string): Promise<number> {
|
||||
try {
|
||||
|
@ -113,20 +99,14 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler {
|
|||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @returns Whether or not the handler is enabled on a site level.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return CoreH5P.canGetTrustedH5PFileInSite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file is downloadable.
|
||||
*
|
||||
* @param file The file data.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns Promise resolved with a boolean and a reason why it isn't downloadable if needed.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isFileDownloadable(file: CoreWSFile, siteId?: string): Promise<CorePluginFileDownloadableResult> {
|
||||
const offlineDisabled = await CoreH5P.isOfflineDisabled(siteId);
|
||||
|
@ -144,31 +124,21 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler {
|
|||
}
|
||||
|
||||
/**
|
||||
* Check whether the file should be treated by this handler. It is used in functions where the component isn't used.
|
||||
*
|
||||
* @param file The file data.
|
||||
* @returns Whether the file should be treated by this handler.
|
||||
* @inheritdoc
|
||||
*/
|
||||
shouldHandleFile(file: CoreWSFile): boolean {
|
||||
return CoreMimetypeUtils.guessExtensionFromUrl(CoreFileHelper.getFileUrl(file)) == 'h5p';
|
||||
}
|
||||
|
||||
/**
|
||||
* Treat a downloaded 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.
|
||||
* @returns Promise resolved when done.
|
||||
* @inheritdoc
|
||||
*/
|
||||
treatDownloadedFile(
|
||||
fileUrl: string,
|
||||
file: FileEntry,
|
||||
siteId?: string,
|
||||
onProgress?: CoreFilepoolOnProgressCallback,
|
||||
options: CorePluginFileTreatDownloadedFileOptions = {},
|
||||
): Promise<void> {
|
||||
return CoreH5PHelper.saveH5P(fileUrl, file, siteId, onProgress);
|
||||
return CoreH5PHelper.saveH5P(fileUrl, file, options);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
"confirmopeninbrowser": "Do you want to open it in a web browser?",
|
||||
"confirmremoveselectedfile": "This will permanently delete '{{filename}}'. You can't undo this.",
|
||||
"confirmremoveselectedfiles": "This will permanently delete selected files. You can't undo this.",
|
||||
"connectandtryagain": "Please connect to the internet and try again.",
|
||||
"connectionlost": "Connection to site lost",
|
||||
"considereddigitalminor": "You are too young to create an account on this site.",
|
||||
"contactsupport": "Contact support",
|
||||
|
@ -212,6 +213,7 @@
|
|||
"mygroups": "My groups",
|
||||
"name": "Name",
|
||||
"needhelp": "Need help?",
|
||||
"needinternettoaccessit": "You need to be connected to the internet to access it.",
|
||||
"networkerroriframemsg": "This content is not available offline. Please connect to the internet and try again.",
|
||||
"networkerrormsg": "There was a problem connecting to the site. Please check your connection and try again.",
|
||||
"never": "Never",
|
||||
|
|
|
@ -748,19 +748,13 @@ export class CoreFilepoolProvider {
|
|||
*
|
||||
* @param siteId The site ID.
|
||||
* @param fileUrl The file URL.
|
||||
* @param options Extra options (revision, timemodified, isexternalfile, repositorytype).
|
||||
* @param filePath Filepath to download the file to. If defined, no extension will be added.
|
||||
* @param onProgress Function to call on progress.
|
||||
* @param poolFileObject When set, the object will be updated, a new entry will not be created.
|
||||
* @param options Extra options.
|
||||
* @returns Resolved with internal URL on success, rejected otherwise.
|
||||
*/
|
||||
protected async downloadForPoolByUrl(
|
||||
siteId: string,
|
||||
fileUrl: string,
|
||||
options: CoreFilepoolFileOptions = {},
|
||||
filePath?: string,
|
||||
onProgress?: CoreFilepoolOnProgressCallback,
|
||||
poolFileObject?: CoreFilepoolFileEntry,
|
||||
options: DownloadForPoolOptions = {},
|
||||
): Promise<string> {
|
||||
const fileId = this.getFileIdByUrl(fileUrl);
|
||||
|
||||
|
@ -771,10 +765,10 @@ export class CoreFilepoolProvider {
|
|||
}
|
||||
|
||||
const extension = CoreMimetypeUtils.guessExtensionFromUrl(fileUrl);
|
||||
const addExtension = filePath === undefined;
|
||||
const path = filePath || (await this.getFilePath(siteId, fileId, extension, fileUrl));
|
||||
const addExtension = options.filePath === undefined;
|
||||
const path = options.filePath || (await this.getFilePath(siteId, fileId, extension, fileUrl));
|
||||
|
||||
if (poolFileObject && poolFileObject.fileId !== fileId) {
|
||||
if (options.poolFileObject && options.poolFileObject.fileId !== fileId) {
|
||||
this.logger.error('Invalid object to update passed');
|
||||
|
||||
throw new CoreError('Invalid object to update passed.');
|
||||
|
@ -794,9 +788,15 @@ export class CoreFilepoolProvider {
|
|||
throw new CoreError(Translate.instant('core.cannotdownloadfiles'));
|
||||
}
|
||||
|
||||
const entry = await CoreWS.downloadFile(fileUrl, path, addExtension, onProgress);
|
||||
const entry = await CoreWS.downloadFile(fileUrl, path, addExtension, options.onProgress);
|
||||
const fileEntry = entry;
|
||||
await CorePluginFileDelegate.treatDownloadedFile(fileUrl, fileEntry, siteId, onProgress);
|
||||
await CorePluginFileDelegate.treatDownloadedFile(fileUrl, fileEntry, {
|
||||
siteId,
|
||||
onProgress: options.onProgress,
|
||||
component: options.component,
|
||||
componentId: options.componentId,
|
||||
timemodified: options.timemodified,
|
||||
});
|
||||
|
||||
await this.addFileToPool(siteId, fileId, {
|
||||
downloadTime: Date.now(),
|
||||
|
@ -1126,7 +1126,14 @@ export class CoreFilepoolProvider {
|
|||
alreadyDownloaded = false;
|
||||
|
||||
try {
|
||||
const url = await this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress);
|
||||
const url = await this.downloadForPoolByUrl(siteId, fileUrl, {
|
||||
...options,
|
||||
filePath,
|
||||
onProgress,
|
||||
component,
|
||||
componentId,
|
||||
timemodified: options.timemodified,
|
||||
});
|
||||
|
||||
return finishSuccessfulDownload(url);
|
||||
} catch (error) {
|
||||
|
@ -1220,7 +1227,7 @@ export class CoreFilepoolProvider {
|
|||
* @param timemodified The timemodified of the file.
|
||||
* @returns Promise resolved with the file data to use.
|
||||
*/
|
||||
protected async fixPluginfileURL(siteId: string, fileUrl: string, timemodified: number = 0): Promise<CoreWSFile> {
|
||||
async fixPluginfileURL(siteId: string, fileUrl: string, timemodified: number = 0): Promise<CoreWSFile> {
|
||||
const file = await CorePluginFileDelegate.getDownloadableFile({ fileurl: fileUrl, timemodified });
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
|
@ -2713,7 +2720,15 @@ export class CoreFilepoolProvider {
|
|||
const onProgress = this.getQueueOnProgress(siteId, fileId);
|
||||
|
||||
try {
|
||||
await this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress, entry);
|
||||
await this.downloadForPoolByUrl(siteId, fileUrl, {
|
||||
...options,
|
||||
filePath,
|
||||
onProgress,
|
||||
poolFileObject: entry,
|
||||
component: item.linksUnserialized?.[0]?.component,
|
||||
componentId: item.linksUnserialized?.[0]?.componentId,
|
||||
timemodified: options.timemodified,
|
||||
});
|
||||
|
||||
// Success, we add links and remove from queue.
|
||||
CorePromiseUtils.ignoreErrors(this.addFileLinks(siteId, fileId, links));
|
||||
|
@ -2752,12 +2767,12 @@ export class CoreFilepoolProvider {
|
|||
dropFromQueue = true;
|
||||
}
|
||||
|
||||
let errorMessage: string | undefined;
|
||||
let error = errorObject;
|
||||
// Some Android devices restrict the amount of usable storage using quotas.
|
||||
// If this quota would be exceeded by the download, it throws an exception.
|
||||
// We catch this exception here, and report a meaningful error message to the user.
|
||||
if (errorObject instanceof FileTransferError && errorObject.exception && errorObject.exception.includes('EDQUOT')) {
|
||||
errorMessage = 'core.course.insufficientavailablequota';
|
||||
error = new Error(Translate.instant('core.course.insufficientavailablequota'));
|
||||
}
|
||||
|
||||
if (dropFromQueue) {
|
||||
|
@ -2765,11 +2780,11 @@ export class CoreFilepoolProvider {
|
|||
|
||||
await CorePromiseUtils.ignoreErrors(this.removeFromQueue(siteId, fileId));
|
||||
|
||||
this.treatQueueDeferred(siteId, fileId, false, errorMessage);
|
||||
this.treatQueueDeferred(siteId, fileId, false, error);
|
||||
this.notifyFileDownloadError(siteId, fileId, links);
|
||||
} else {
|
||||
// We considered the file as legit but did not get it, failure.
|
||||
this.treatQueueDeferred(siteId, fileId, false, errorMessage);
|
||||
this.treatQueueDeferred(siteId, fileId, false, error);
|
||||
this.notifyFileDownloadError(siteId, fileId, links);
|
||||
|
||||
throw errorObject;
|
||||
|
@ -3124,12 +3139,12 @@ export class CoreFilepoolProvider {
|
|||
* @param resolve True if promise should be resolved, false if it should be rejected.
|
||||
* @param error String identifier for error message, if rejected.
|
||||
*/
|
||||
protected treatQueueDeferred(siteId: string, fileId: string, resolve: boolean, error?: string): void {
|
||||
protected treatQueueDeferred(siteId: string, fileId: string, resolve: boolean, error?: Error | string): void {
|
||||
if (siteId in this.queueDeferreds && fileId in this.queueDeferreds[siteId]) {
|
||||
if (resolve) {
|
||||
this.queueDeferreds[siteId][fileId].resolve();
|
||||
} else {
|
||||
this.queueDeferreds[siteId][fileId].reject(new Error(error));
|
||||
this.queueDeferreds[siteId][fileId].reject(typeof error === 'string' ? new Error(error) : error);
|
||||
}
|
||||
delete this.queueDeferreds[siteId][fileId];
|
||||
}
|
||||
|
@ -3241,3 +3256,15 @@ type CoreFilepoolPromisedValue = CorePromisedValue<void> & {
|
|||
|
||||
type AnchorOrMediaElement =
|
||||
HTMLAnchorElement | HTMLImageElement | HTMLAudioElement | HTMLVideoElement | HTMLSourceElement | HTMLTrackElement;
|
||||
|
||||
/**
|
||||
* Options for downloadForPoolByUrl.
|
||||
*/
|
||||
type DownloadForPoolOptions = CoreFilepoolFileOptions & {
|
||||
filePath?: string; // Filepath to download the file to. If defined, no extension will be added.
|
||||
onProgress?: CoreFilepoolOnProgressCallback; // Function to call on progress.
|
||||
poolFileObject?: CoreFilepoolFileEntry; // When set, the object will be updated, a new entry will not be created.
|
||||
component?: string; // The component to link the file to.
|
||||
componentId?: string | number; // An ID to use in conjunction with the component.
|
||||
timemodified?: number; // The time the file was modified.
|
||||
};
|
||||
|
|
|
@ -271,20 +271,18 @@ export class CorePluginFileDelegateService extends CoreDelegate<CorePluginFileHa
|
|||
*
|
||||
* @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.
|
||||
* @param options Options.
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
async treatDownloadedFile(
|
||||
fileUrl: string,
|
||||
file: FileEntry,
|
||||
siteId?: string,
|
||||
onProgress?: CoreFilepoolOnProgressCallback,
|
||||
options: CorePluginFileTreatDownloadedFileOptions = {},
|
||||
): Promise<void> {
|
||||
const handler = this.getHandlerForFile({ fileurl: fileUrl });
|
||||
|
||||
if (handler && handler.treatDownloadedFile) {
|
||||
await handler.treatDownloadedFile(fileUrl, file, siteId, onProgress);
|
||||
await handler.treatDownloadedFile(fileUrl, file, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -378,16 +376,14 @@ export interface CorePluginFileHandler extends CoreDelegateHandler {
|
|||
*
|
||||
* @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.
|
||||
* @param options Options.
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
treatDownloadedFile?(
|
||||
fileUrl: string,
|
||||
file: FileEntry,
|
||||
siteId?: string,
|
||||
onProgress?: CoreFilepoolOnProgressCallback):
|
||||
Promise<void>;
|
||||
options?: CorePluginFileTreatDownloadedFileOptions,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -412,3 +408,14 @@ export type CoreFileSizeSum = {
|
|||
size: number; // Sum of file sizes.
|
||||
total: boolean; // False if any file size is not available.
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for treatDownloadedFile.
|
||||
*/
|
||||
export type CorePluginFileTreatDownloadedFileOptions<T = unknown> = {
|
||||
siteId?: string; // Site ID. If not defined, current site.
|
||||
onProgress?: CoreFilepoolOnProgressCallback<T>; // Function to call on progress.
|
||||
component?: string; // The component to link the file to.
|
||||
componentId?: string | number; // An ID to use in conjunction with the component.
|
||||
timemodified?: number; // The timemodified of the file.
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@ For more information about upgrading, read the official documentation: https://m
|
|||
=== 5.0.0 ===
|
||||
|
||||
- The logout process has been refactored, now it uses a logout page to trigger Angular guards. CoreSites.logout now uses this process, and CoreSites.logoutForRedirect is deprecated and shouldn't be used anymore.
|
||||
- The parameters of treatDownloadedFile of plugin file handlers have changed. Now the third parameter is an object with all the optional parameters.
|
||||
|
||||
=== 4.5.0 ===
|
||||
|
||||
|
|
Loading…
Reference in New Issue