diff --git a/scripts/langindex.json b/scripts/langindex.json
index dd460ea09..480df6cfd 100644
--- a/scripts/langindex.json
+++ b/scripts/langindex.json
@@ -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",
diff --git a/src/addons/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html b/src/addons/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html
index fbff14346..3918ef7a1 100644
--- a/src/addons/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html
+++ b/src/addons/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html
@@ -27,15 +27,21 @@
-
-
+
+ @if (triedToPlay && !isOnline && (!siteCanDownload || hasMissingDependencies)) {
+
+ @if (!siteCanDownload) {
{{ 'core.h5p.offlinedisabled' | translate }} {{ 'addon.mod_h5pactivity.offlinedisabledwarning' | translate }}
+ } @else {
+ {{ 'core.course.activitynotavailableoffline' | translate }} {{ 'core.needinternettoaccessit' | translate }}
+ }
+ }
@@ -69,7 +75,7 @@
+ [state]="contentState" [component]="component" [componentId]="componentId" [fileTimemodified]="deployedFile?.timemodified" />
diff --git a/src/addons/mod/h5pactivity/components/index/index.ts b/src/addons/mod/h5pactivity/components/index/index.ts
index 9fcc02cd2..3961402ec 100644
--- a/src/addons/mod/h5pactivity/components/index/index.ts
+++ b/src/addons/mod/h5pactivity/components/index/index.ts
@@ -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;
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(() => {
diff --git a/src/addons/mod/h5pactivity/services/h5pactivity.ts b/src/addons/mod/h5pactivity/services/h5pactivity.ts
index 7065efe8e..d34354fd2 100644
--- a/src/addons/mod/h5pactivity/services/h5pactivity.ts
+++ b/src/addons/mod/h5pactivity/services/h5pactivity.ts
@@ -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 {
+ 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 {
+ const missingDependencies = await this.getMissingDependencies(componentId, deployedFile, siteId);
+
+ return missingDependencies.length > 0;
+ }
+
/**
* Invalidates access information.
*
diff --git a/src/addons/mod/h5pactivity/services/handlers/prefetch.ts b/src/addons/mod/h5pactivity/services/handlers/prefetch.ts
index 2985e7d0f..467014058 100644
--- a/src/addons/mod/h5pactivity/services/handlers/prefetch.ts
+++ b/src/addons/mod/h5pactivity/services/handlers/prefetch.ts
@@ -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 {
+ // 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);
diff --git a/src/core/features/course/lang.json b/src/core/features/course/lang.json
index 0717512b9..b7a8a33cb 100644
--- a/src/core/features/course/lang.json
+++ b/src/core/features/course/lang.json
@@ -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.
Connect your device to the internet to avoid losing your progress.",
"communicationroomlink": "Chat to course participants",
"completion_automatic:done": "Done:",
"completion_automatic:failed": "Failed:",
diff --git a/src/core/features/h5p/classes/errors/missing-dependencies-error.ts b/src/core/features/h5p/classes/errors/missing-dependencies-error.ts
new file mode 100644
index 000000000..6b96b7552
--- /dev/null
+++ b/src/core/features/h5p/classes/errors/missing-dependencies-error.ts
@@ -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;
+ }
+
+}
diff --git a/src/core/features/h5p/classes/framework.ts b/src/core/features/h5p/classes/framework.ts
index e2b3c6afb..1747ceee9 100644
--- a/src/core/features/h5p/classes/framework.ts
+++ b/src/core/features/h5p/classes/framework.ts
@@ -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>>;
protected contentsLibrariesTables: LazyMap>>;
protected librariesCachedAssetsTables: LazyMap>>;
+ protected missingDependenciesTables: LazyMap<
+ AsyncInstance>
+ >;
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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
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 {
+ 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 & {
addTo: string;
};
+
+/**
+ * Options for storeMissingDependencies.
+ */
+type StoreMissingDependenciesOptions = {
+ component?: string;
+ componentId?: string | number;
+ fileTimemodified?: number;
+ siteId?: string;
+};
diff --git a/src/core/features/h5p/classes/helper.ts b/src/core/features/h5p/classes/helper.ts
index 375f7add3..6b44844eb 100644
--- a/src/core/features/h5p/classes/helper.ts
+++ b/src/core/features/h5p/classes/helper.ts
@@ -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 {
- siteId = siteId || CoreSites.getCurrentSiteId();
+ static async saveH5P(
+ fileUrl: string,
+ file: FileEntry,
+ options: CorePluginFileTreatDownloadedFileOptions = {},
+ ): Promise {
+ 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 = {},
): Promise {
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;
diff --git a/src/core/features/h5p/classes/validator.ts b/src/core/features/h5p/classes/validator.ts
index d72545705..0612bec96 100644
--- a/src/core/features/h5p/classes/validator.ts
+++ b/src/core/features/h5p/classes/validator.ts
@@ -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 };
diff --git a/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts b/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts
index d3542f469..389472e77 100644
--- a/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts
+++ b/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts
@@ -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();
@@ -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(
diff --git a/src/core/features/h5p/components/h5p-player/core-h5p-player.html b/src/core/features/h5p/components/h5p-player/core-h5p-player.html
index d5948426d..e30f0435e 100644
--- a/src/core/features/h5p/components/h5p-player/core-h5p-player.html
+++ b/src/core/features/h5p/components/h5p-player/core-h5p-player.html
@@ -9,4 +9,5 @@
-
+
diff --git a/src/core/features/h5p/components/h5p-player/h5p-player.ts b/src/core/features/h5p/components/h5p-player/h5p-player.ts
index 41cc6ac3b..95853d05c 100644
--- a/src/core/features/h5p/components/h5p-player/h5p-player.ts
+++ b/src/core/features/h5p/components/h5p-player/h5p-player.ts
@@ -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);
diff --git a/src/core/features/h5p/services/database/h5p.ts b/src/core/features/h5p/services/database/h5p.ts
index 4ff57daa5..92814e473 100644
--- a/src/core/features/h5p/services/database/h5p.ts
+++ b/src/core/features/h5p/services/database/h5p.ts
@@ -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 {
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];
diff --git a/src/core/features/h5p/services/h5p.ts b/src/core/features/h5p/services/h5p.ts
index d3283b853..29b790d7b 100644
--- a/src/core/features/h5p/services/h5p.ts
+++ b/src/core/features/h5p/services/h5p.ts
@@ -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 {
+ 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.
*
diff --git a/src/core/features/h5p/services/handlers/pluginfile.ts b/src/core/features/h5p/services/handlers/pluginfile.ts
index 7e04da07c..eca32e25f 100644
--- a/src/core/features/h5p/services/handlers/pluginfile.ts
+++ b/src/core/features/h5p/services/handlers/pluginfile.ts
@@ -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 {
// 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 {
- 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 = 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 {
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 {
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 {
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 {
- return CoreH5PHelper.saveH5P(fileUrl, file, siteId, onProgress);
+ return CoreH5PHelper.saveH5P(fileUrl, file, options);
}
}
diff --git a/src/core/lang.json b/src/core/lang.json
index 890ed0331..a4a3981d3 100644
--- a/src/core/lang.json
+++ b/src/core/lang.json
@@ -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",
diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts
index 5151214d2..ba760474a 100644
--- a/src/core/services/filepool.ts
+++ b/src/core/services/filepool.ts
@@ -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 {
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 {
+ async fixPluginfileURL(siteId: string, fileUrl: string, timemodified: number = 0): Promise {
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 & {
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.
+};
diff --git a/src/core/services/plugin-file-delegate.ts b/src/core/services/plugin-file-delegate.ts
index f7aa0012a..5ac8a0bdd 100644
--- a/src/core/services/plugin-file-delegate.ts
+++ b/src/core/services/plugin-file-delegate.ts
@@ -271,20 +271,18 @@ export class CorePluginFileDelegateService extends CoreDelegate {
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;
+ options?: CorePluginFileTreatDownloadedFileOptions,
+ ): Promise;
}
/**
@@ -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 = {
+ siteId?: string; // Site ID. If not defined, current site.
+ onProgress?: CoreFilepoolOnProgressCallback; // 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.
+};
diff --git a/upgrade.txt b/upgrade.txt
index a9429f01f..d171afb51 100644
--- a/upgrade.txt
+++ b/upgrade.txt
@@ -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 ===