diff --git a/scripts/langindex.json b/scripts/langindex.json
index 373656b89..344e7460d 100644
--- a/scripts/langindex.json
+++ b/scripts/langindex.json
@@ -1619,6 +1619,7 @@
   "core.h5p.offlineDialogRetryButtonLabel": "h5p",
   "core.h5p.offlineDialogRetryMessage": "h5p",
   "core.h5p.offlineSuccessfulSubmit": "h5p",
+  "core.h5p.offlinedisabled": "local_moodlemobileapp",
   "core.h5p.originator": "h5p",
   "core.h5p.pd": "h5p",
   "core.h5p.pddl": "h5p",
diff --git a/src/addon/mod/book/components/index/index.ts b/src/addon/mod/book/components/index/index.ts
index 40717a6fc..ee66c3b9a 100644
--- a/src/addon/mod/book/components/index/index.ts
+++ b/src/addon/mod/book/components/index/index.ts
@@ -119,6 +119,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
     protected fetchContent(refresh?: boolean): Promise<any> {
         const promises = [];
         let downloadFailed = false;
+        let downloadFailError;
 
         // Try to get the book data.
         promises.push(this.bookProvider.getBook(this.courseId, this.module.id).then((book) => {
@@ -129,9 +130,10 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
         }));
 
         // Download content. This function also loads module contents if needed.
-        promises.push(this.prefetchDelegate.download(this.module, this.courseId).catch(() => {
+        promises.push(this.prefetchDelegate.download(this.module, this.courseId).catch((error) => {
             // Mark download as failed but go on since the main files could have been downloaded.
             downloadFailed = true;
+            downloadFailError = error;
 
             if (!this.module.contents.length) {
                 // Try to load module contents for offline usage.
@@ -163,7 +165,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
             return this.loadChapter(this.currentChapter).then(() => {
                 if (downloadFailed && this.appProvider.isOnline()) {
                     // We could load the main file but the download failed. Show error message.
-                    this.domUtils.showErrorModal('core.errordownloadingsomefiles', true);
+                    this.showErrorDownloadingSomeFiles(downloadFailError);
                 }
             }).catch(() => {
                 // Ignore errors, they're handled inside the loadChapter function.
diff --git a/src/addon/mod/imscp/components/index/index.ts b/src/addon/mod/imscp/components/index/index.ts
index c6690b324..a17aa8de4 100644
--- a/src/addon/mod/imscp/components/index/index.ts
+++ b/src/addon/mod/imscp/components/index/index.ts
@@ -76,6 +76,7 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
      */
     protected fetchContent(refresh?: boolean): Promise<any> {
         let downloadFailed = false;
+        let downloadFailError;
         const promises = [];
 
         promises.push(this.imscpProvider.getImscp(this.courseId, this.module.id).then((imscp) => {
@@ -83,9 +84,10 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
             this.dataRetrieved.emit(imscp);
         }));
 
-        promises.push(this.imscpPrefetch.download(this.module, this.courseId).catch(() => {
+        promises.push(this.imscpPrefetch.download(this.module, this.courseId).catch((error) => {
             // Mark download as failed but go on since the main files could have been downloaded.
             downloadFailed = true;
+            downloadFailError = error;
 
             return this.courseProvider.loadModuleContents(this.module, this.courseId).catch((error) => {
                 // Error getting module contents, fail.
@@ -109,7 +111,7 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
         }).then(() => {
             if (downloadFailed && this.appProvider.isOnline()) {
                 // We could load the main file but the download failed. Show error message.
-                this.domUtils.showErrorModal('core.errordownloadingsomefiles', true);
+                this.showErrorDownloadingSomeFiles(downloadFailError);
             }
 
         }).finally(() => {
diff --git a/src/addon/mod/page/components/index/index.ts b/src/addon/mod/page/components/index/index.ts
index 9ee38717e..b8300246a 100644
--- a/src/addon/mod/page/components/index/index.ts
+++ b/src/addon/mod/page/components/index/index.ts
@@ -78,11 +78,13 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp
      */
     protected fetchContent(refresh?: boolean): Promise<any> {
         let downloadFailed = false;
+        let downloadFailError;
 
         // Download content. This function also loads module contents if needed.
-        return this.pagePrefetch.download(this.module, this.courseId).catch(() => {
+        return this.pagePrefetch.download(this.module, this.courseId).catch((error) => {
             // Mark download as failed but go on since the main files could have been downloaded.
             downloadFailed = true;
+            downloadFailError = error;
         }).then(() => {
             if (!this.module.contents.length) {
                 // Try to load module contents for offline usage.
@@ -132,7 +134,7 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp
 
                 if (downloadFailed && this.appProvider.isOnline()) {
                     // We could load the main file but the download failed. Show error message.
-                    this.domUtils.showErrorModal('core.errordownloadingsomefiles', true);
+                    this.showErrorDownloadingSomeFiles(downloadFailError);
                 }
             }));
 
diff --git a/src/addon/mod/resource/components/index/index.ts b/src/addon/mod/resource/components/index/index.ts
index 12913fe40..8875bae06 100644
--- a/src/addon/mod/resource/components/index/index.ts
+++ b/src/addon/mod/resource/components/index/index.ts
@@ -14,6 +14,7 @@
 
 import { Component, Injector } from '@angular/core';
 import { CoreAppProvider } from '@providers/app';
+import { CoreFilepoolProvider } from '@providers/filepool';
 import { CoreSitesProvider } from '@providers/sites';
 import { CoreUtilsProvider } from '@providers/utils/utils';
 import { CoreCourseProvider } from '@core/course/providers/course';
@@ -38,10 +39,15 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
     contentText: string;
     displayDescription = true;
 
-    constructor(injector: Injector, private resourceProvider: AddonModResourceProvider, private courseProvider: CoreCourseProvider,
-            private appProvider: CoreAppProvider, private prefetchHandler: AddonModResourcePrefetchHandler,
-            private resourceHelper: AddonModResourceHelperProvider, private sitesProvider: CoreSitesProvider,
-            private utils: CoreUtilsProvider) {
+    constructor(injector: Injector,
+            protected resourceProvider: AddonModResourceProvider,
+            protected courseProvider: CoreCourseProvider,
+            protected appProvider: CoreAppProvider,
+            protected prefetchHandler: AddonModResourcePrefetchHandler,
+            protected resourceHelper: AddonModResourceHelperProvider,
+            protected sitesProvider: CoreSitesProvider,
+            protected utils: CoreUtilsProvider,
+            protected filepoolProvider: CoreFilepoolProvider) {
         super(injector);
     }
 
@@ -104,10 +110,12 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
 
             if (this.resourceHelper.isDisplayedInIframe(this.module)) {
                 let downloadFailed = false;
+                let downloadFailError;
 
-                return this.prefetchHandler.download(this.module, this.courseId).catch(() => {
+                return this.prefetchHandler.download(this.module, this.courseId).catch((error) => {
                     // Mark download as failed but go on since the main files could have been downloaded.
                     downloadFailed = true;
+                    downloadFailError = error;
                 }).then(() => {
                     return this.resourceHelper.getIframeSrc(this.module).then((src) => {
                         this.mode = 'iframe';
@@ -125,7 +133,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
 
                         if (downloadFailed && this.appProvider.isOnline()) {
                             // We could load the main file but the download failed. Show error message.
-                            this.domUtils.showErrorModal('core.errordownloadingsomefiles', true);
+                            this.showErrorDownloadingSomeFiles(downloadFailError);
                         }
                     });
                 });
@@ -147,15 +155,23 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
 
     /**
      * Opens a file.
+     *
+     * @return Promise resolved when done.
      */
-    open(): void {
-        this.prefetchHandler.isDownloadable(this.module, this.courseId).then((downloadable) => {
+    async open(): Promise<void> {
+        let downloadable = await this.prefetchHandler.isDownloadable(this.module, this.courseId);
+
+        if (downloadable) {
+            // Check if the main file is downloadle.
+            // This isn't done in "isDownloadable" to prevent extra WS calls in the course page.
+            downloadable = await this.resourceHelper.isMainFileDownloadable(this.module);
+
             if (downloadable) {
-                this.resourceHelper.openModuleFile(this.module, this.courseId);
-            } else {
-                // The resource cannot be downloaded, open the activity in browser.
-                return this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(this.module.url);
+                return this.resourceHelper.openModuleFile(this.module, this.courseId);
             }
-        });
+        }
+
+        // The resource cannot be downloaded, open the activity in browser.
+        return this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(this.module.url);
     }
 }
diff --git a/src/addon/mod/resource/providers/helper.ts b/src/addon/mod/resource/providers/helper.ts
index 401783103..cdbf17270 100644
--- a/src/addon/mod/resource/providers/helper.ts
+++ b/src/addon/mod/resource/providers/helper.ts
@@ -20,6 +20,7 @@ import { AddonModResourceProvider } from './resource';
 import { CoreSitesProvider } from '@providers/sites';
 import { CoreFilepoolProvider } from '@providers/filepool';
 import { CoreFileProvider } from '@providers/file';
+import { CoreFileHelperProvider } from '@providers/file-helper';
 import { CoreAppProvider } from '@providers/app';
 import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
 import { CoreTextUtilsProvider } from '@providers/utils/text';
@@ -36,11 +37,17 @@ export class AddonModResourceHelperProvider {
     // Display using object tag.
     protected DISPLAY_EMBED = 1;
 
-    constructor(private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider,
-            private resourceProvider: AddonModResourceProvider, private courseHelper: CoreCourseHelperProvider,
-            private textUtils: CoreTextUtilsProvider, private mimetypeUtils: CoreMimetypeUtilsProvider,
-            private fileProvider: CoreFileProvider, private appProvider: CoreAppProvider,
-            private filepoolProvider: CoreFilepoolProvider, private sitesProvider: CoreSitesProvider) {
+    constructor(protected courseProvider: CoreCourseProvider,
+            protected domUtils: CoreDomUtilsProvider,
+            protected resourceProvider: AddonModResourceProvider,
+            protected courseHelper: CoreCourseHelperProvider,
+            protected textUtils: CoreTextUtilsProvider,
+            protected mimetypeUtils: CoreMimetypeUtilsProvider,
+            protected fileProvider: CoreFileProvider,
+            protected appProvider: CoreAppProvider,
+            protected filepoolProvider: CoreFilepoolProvider,
+            protected sitesProvider: CoreSitesProvider,
+            protected fileHelper: CoreFileHelperProvider) {
     }
 
     /**
@@ -136,6 +143,23 @@ export class AddonModResourceHelperProvider {
         return mimetype == 'text/html';
     }
 
+    /**
+     * Check if main file of resource is downloadable.
+     *
+     * @param module Module instance.
+     * @param siteId Site ID. If not defined, current site.
+     * @return Promise resolved with boolean: whether main file is downloadable.
+     */
+    isMainFileDownloadable(module: any, siteId?: string): Promise<boolean> {
+        siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+        const mainFile = module.contents[0];
+        const fileUrl = this.fileHelper.getFileUrl(mainFile);
+        const timemodified = this.fileHelper.getFileTimemodified(mainFile);
+
+        return this.filepoolProvider.isFileDownloadable(siteId, fileUrl, timemodified);
+    }
+
     /**
      * Check if the resource is a Nextcloud file.
      *
diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json
index 769a0c480..e9673f525 100644
--- a/src/assets/lang/en.json
+++ b/src/assets/lang/en.json
@@ -1619,6 +1619,7 @@
     "core.h5p.offlineDialogRetryButtonLabel": "Retry now",
     "core.h5p.offlineDialogRetryMessage": "Retrying in :num....",
     "core.h5p.offlineSuccessfulSubmit": "Successfully submitted results.",
+    "core.h5p.offlinedisabled": "The site doesn't allow downloading H5P packages.",
     "core.h5p.originator": "Originator",
     "core.h5p.pd": "Public Domain",
     "core.h5p.pddl": "Public Domain Dedication and Licence",
diff --git a/src/components/file/file.ts b/src/components/file/file.ts
index 57c509107..22534fe16 100644
--- a/src/components/file/file.ts
+++ b/src/components/file/file.ts
@@ -20,6 +20,7 @@ import { CoreFileHelperProvider } from '@providers/file-helper';
 import { CoreSitesProvider } from '@providers/sites';
 import { CoreDomUtilsProvider } from '@providers/utils/dom';
 import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
+import { CoreUrlUtilsProvider } from '@providers/utils/url';
 import { CoreUtilsProvider } from '@providers/utils/utils';
 import { CoreTextUtilsProvider } from '@providers/utils/text';
 import { CoreConstants } from '@core/constants';
@@ -57,16 +58,17 @@ export class CoreFileComponent implements OnInit, OnDestroy {
     protected timemodified: number;
     protected observer;
 
-    constructor(private sitesProvider: CoreSitesProvider,
-            private utils: CoreUtilsProvider,
-            private domUtils: CoreDomUtilsProvider,
-            private filepoolProvider: CoreFilepoolProvider,
-            private appProvider: CoreAppProvider,
-            private fileHelper: CoreFileHelperProvider,
-            private mimeUtils: CoreMimetypeUtilsProvider,
-            private eventsProvider: CoreEventsProvider,
-            private textUtils: CoreTextUtilsProvider,
-            private pluginFileDelegate: CorePluginFileDelegate) {
+    constructor(protected sitesProvider: CoreSitesProvider,
+            protected utils: CoreUtilsProvider,
+            protected domUtils: CoreDomUtilsProvider,
+            protected filepoolProvider: CoreFilepoolProvider,
+            protected appProvider: CoreAppProvider,
+            protected fileHelper: CoreFileHelperProvider,
+            protected mimeUtils: CoreMimetypeUtilsProvider,
+            protected eventsProvider: CoreEventsProvider,
+            protected textUtils: CoreTextUtilsProvider,
+            protected pluginFileDelegate: CorePluginFileDelegate,
+            protected urlUtils: CoreUrlUtilsProvider) {
         this.onDelete = new EventEmitter();
     }
 
@@ -104,6 +106,8 @@ export class CoreFileComponent implements OnInit, OnDestroy {
                 this.observer = this.eventsProvider.on(eventName, () => {
                     this.calculateState();
                 });
+            }).catch(() => {
+                // File not downloadable.
             });
         }
     }
@@ -152,14 +156,14 @@ export class CoreFileComponent implements OnInit, OnDestroy {
             return;
         }
 
-        if (!this.canDownload) {
+        if (!this.canDownload || !this.state || this.state == CoreConstants.NOT_DOWNLOADABLE) {
             // File cannot be downloaded, just open it.
             if (this.file.toURL) {
                 // Local file.
                 this.utils.openFile(this.file.toURL());
             } else if (this.fileUrl) {
                 if (this.fileUrl.indexOf('http') === 0) {
-                    this.utils.openOnlineFile(this.fileUrl);
+                    this.utils.openOnlineFile(this.urlUtils.unfixPluginfileURL(this.fileUrl));
                 } else {
                     this.utils.openFile(this.fileUrl);
                 }
diff --git a/src/core/course/classes/main-resource-component.ts b/src/core/course/classes/main-resource-component.ts
index 87691f7d8..50f432894 100644
--- a/src/core/course/classes/main-resource-component.ts
+++ b/src/core/course/classes/main-resource-component.ts
@@ -17,7 +17,7 @@ import { NavController } from 'ionic-angular';
 import { TranslateService } from '@ngx-translate/core';
 import { CoreLoggerProvider } from '@providers/logger';
 import { CoreDomUtilsProvider } from '@providers/utils/dom';
-import { CoreTextUtilsProvider } from '@providers/utils/text';
+import { CoreTextUtilsProvider, CoreTextErrorObject } from '@providers/utils/text';
 import { CoreCourseHelperProvider } from '@core/course/providers/helper';
 import { CoreCourseModuleMainComponent, CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
 import { CoreCourseSectionPage } from '@core/course/pages/section/section.ts';
@@ -265,6 +265,20 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
         this.courseHelper.confirmAndRemoveFiles(this.module, this.courseId);
     }
 
+    /**
+     * Show an error occurred while downloading files.
+     *
+     * @param error The specific error.
+     */
+    protected showErrorDownloadingSomeFiles(error: string | CoreTextErrorObject): void {
+        const errorMessage = this.textUtils.buildSeveralParagraphsMessage([
+            this.translate.instant('core.errordownloadingsomefiles'),
+            error,
+        ]);
+
+        this.domUtils.showErrorModal(errorMessage);
+    }
+
     /**
      * Component being destroyed.
      */
diff --git a/src/core/courses/providers/course-link-handler.ts b/src/core/courses/providers/course-link-handler.ts
index b94813f4c..c7fdd7df4 100644
--- a/src/core/courses/providers/course-link-handler.ts
+++ b/src/core/courses/providers/course-link-handler.ts
@@ -178,8 +178,9 @@ export class CoreCoursesCourseLinkHandler extends CoreContentLinksHandlerBase {
                         error = this.translate.instant('core.courses.notenroled');
                     }
 
-                    const body = this.translate.instant('core.twoparagraphs',
-                        { p1: error, p2: this.translate.instant('core.confirmopeninbrowser') });
+                    const body = this.textUtils.buildSeveralParagraphsMessage(
+                            [error, this.translate.instant('core.confirmopeninbrowser')]);
+
                     this.domUtils.showConfirm(body).then(() => {
                         this.sitesProvider.getCurrentSite().openInBrowserWithAutoLogin(url);
                     }).catch(() => {
diff --git a/src/core/h5p/components/h5p-player/h5p-player.ts b/src/core/h5p/components/h5p-player/h5p-player.ts
index 4804c4dbf..25153b69c 100644
--- a/src/core/h5p/components/h5p-player/h5p-player.ts
+++ b/src/core/h5p/components/h5p-player/h5p-player.ts
@@ -73,7 +73,8 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
         this.logger = loggerProvider.getInstance('CoreH5PPlayerComponent');
         this.site = sitesProvider.getCurrentSite();
         this.siteId = this.site.getId();
-        this.siteCanDownload = this.sitesProvider.getCurrentSite().canDownloadFiles();
+        this.siteCanDownload = this.sitesProvider.getCurrentSite().canDownloadFiles() &&
+                !this.h5pProvider.isOfflineDisabledInSite();
     }
 
     /**
diff --git a/src/core/h5p/lang/en.json b/src/core/h5p/lang/en.json
index 923542f14..d661721f0 100644
--- a/src/core/h5p/lang/en.json
+++ b/src/core/h5p/lang/en.json
@@ -64,6 +64,7 @@
     "offlineDialogRetryButtonLabel": "Retry now",
     "offlineDialogRetryMessage": "Retrying in :num....",
     "offlineSuccessfulSubmit": "Successfully submitted results.",
+    "offlinedisabled": "The site doesn't allow downloading H5P packages.",
     "originator": "Originator",
     "pd": "Public Domain",
     "pddl": "Public Domain Dedication and Licence",
diff --git a/src/core/h5p/providers/h5p.ts b/src/core/h5p/providers/h5p.ts
index 6750e226c..e332278d6 100644
--- a/src/core/h5p/providers/h5p.ts
+++ b/src/core/h5p/providers/h5p.ts
@@ -1876,6 +1876,30 @@ export class CoreH5PProvider {
         });
     }
 
+    /**
+     * Check whether H5P offline is disabled.
+     *
+     * @param siteId Site ID. If not defined, current site.
+     * @return Promise resolved with boolean: whether is disabled.
+     */
+    async isOfflineDisabled(siteId?: string): Promise<boolean> {
+        const site = await this.sitesProvider.getSite(siteId);
+
+        return this.isOfflineDisabledInSite(site);
+    }
+
+    /**
+     * Check whether H5P offline is disabled.
+     *
+     * @param site Site instance. If not defined, current site.
+     * @return Whether is disabled.
+     */
+    isOfflineDisabledInSite(site?: CoreSite): boolean {
+        site = site || this.sitesProvider.getCurrentSite();
+
+        return site.isFeatureDisabled('NoDelegate_H5POffline');
+    }
+
     /**
      * Performs actions required when a library has been installed.
      *
diff --git a/src/core/h5p/providers/pluginfile-handler.ts b/src/core/h5p/providers/pluginfile-handler.ts
index 3a38e6381..8f0ca98d7 100644
--- a/src/core/h5p/providers/pluginfile-handler.ts
+++ b/src/core/h5p/providers/pluginfile-handler.ts
@@ -22,6 +22,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
 import { CoreH5PProvider } from './h5p';
 import { CoreWSExternalFile } from '@providers/ws';
 import { FileEntry } from '@ionic-native/file';
+import { TranslateService } from '@ngx-translate/core';
 
 /**
  * Handler to treat H5P files.
@@ -35,7 +36,8 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
             protected textUtils: CoreTextUtilsProvider,
             protected utils: CoreUtilsProvider,
             protected fileProvider: CoreFileProvider,
-            protected h5pProvider: CoreH5PProvider) { }
+            protected h5pProvider: CoreH5PProvider,
+            protected translate: TranslateService) { }
 
     /**
      * React to a file being deleted.
@@ -112,6 +114,28 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
         return this.h5pProvider.canGetTrustedH5PFileInSite();
     }
 
+    /**
+     * Check if a file is downloadable.
+     *
+     * @param file The file data.
+     * @param siteId Site ID. If not defined, current site.
+     * @return Promise resolved with a boolean and a reason why it isn't downloadable if needed.
+     */
+    async isFileDownloadable(file: CoreWSExternalFile, siteId?: string): Promise<{downloadable: boolean, reason?: string}> {
+        const offlineDisabled = await this.h5pProvider.isOfflineDisabled(siteId);
+
+        if (offlineDisabled) {
+            return {
+                downloadable: false,
+                reason: this.translate.instant('core.h5p.offlinedisabled'),
+            };
+        } else {
+            return {
+                downloadable: true,
+            };
+        }
+    }
+
     /**
      * Check whether the file should be treated by this handler. It is used in functions where the component isn't used.
      *
diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts
index 11e53f420..a4faa08f3 100644
--- a/src/providers/filepool.ts
+++ b/src/providers/filepool.ts
@@ -2385,6 +2385,23 @@ export class CoreFilepoolProvider {
         });
     }
 
+    /**
+     * Check whether a file is downloadable.
+     *
+     * @param siteId The site ID.
+     * @param fileUrl File URL.
+     * @param timemodified The time this file was modified.
+     * @param filePath Filepath to download the file to. If defined, no extension will be added.
+     * @param revision File revision. If not defined, it will be calculated using the URL.
+     * @return Promise resolved with a boolean: whether a file is downloadable.
+     */
+    async isFileDownloadable(siteId: string, fileUrl: string, timemodified: number = 0, filePath?: string, revision?: number)
+            : Promise<boolean> {
+        const state = await this.getFileStateByUrl(siteId, fileUrl, timemodified, filePath, revision);
+
+        return state != CoreConstants.NOT_DOWNLOADABLE;
+    }
+
     /**
      * Check if a file is downloading.
      *
diff --git a/src/providers/plugin-file-delegate.ts b/src/providers/plugin-file-delegate.ts
index 469518698..42ee781a7 100644
--- a/src/providers/plugin-file-delegate.ts
+++ b/src/providers/plugin-file-delegate.ts
@@ -84,6 +84,15 @@ export interface CorePluginFileHandler extends CoreDelegateHandler {
      */
     getFileSize?(file: CoreWSExternalFile, siteId?: string): Promise<number>;
 
+    /**
+     * Check if a file is downloadable.
+     *
+     * @param file The file data.
+     * @param siteId Site ID. If not defined, current site.
+     * @return Promise resolved with a boolean and a reason why it isn't downloadable if needed.
+     */
+    isFileDownloadable?(file: CoreWSExternalFile, siteId?: string): Promise<CorePluginFileDownloadableResult>;
+
     /**
      * Check whether the file should be treated by this handler. It is used in functions where the component isn't used.
      *
@@ -103,6 +112,21 @@ export interface CorePluginFileHandler extends CoreDelegateHandler {
     treatDownloadedFile?(fileUrl: string, file: FileEntry, siteId?: string): Promise<any>;
 }
 
+/**
+ * Data about if a file is downloadable.
+ */
+export type CorePluginFileDownloadableResult = {
+    /**
+     * Whether it's downloadable.
+     */
+    downloadable: boolean;
+
+    /**
+     * If not downloadable, the reason why it isn't.
+     */
+    reason?: string;
+};
+
 /**
  * Delegate to register pluginfile information handlers.
  */
@@ -155,16 +179,22 @@ export class CorePluginFileDelegate extends CoreDelegate {
      * @param siteId Site ID. If not defined, current site.
      * @return Promise resolved with the file to use. Rejected if cannot download.
      */
-    protected getHandlerDownloadableFile(file: CoreWSExternalFile, handler: CorePluginFileHandler, siteId?: string)
+    protected async getHandlerDownloadableFile(file: CoreWSExternalFile, handler: CorePluginFileHandler, siteId?: string)
             : Promise<CoreWSExternalFile> {
 
-        if (handler && handler.getDownloadableFile) {
-            return handler.getDownloadableFile(file, siteId).then((newFile) => {
-                return newFile || file;
-            });
+        const isDownloadable = await this.isFileDownloadable(file, siteId);
+
+        if (!isDownloadable.downloadable) {
+            throw isDownloadable.reason;
         }
 
-        return Promise.resolve(file);
+        if (handler && handler.getDownloadableFile) {
+            const newFile = await handler.getDownloadableFile(file, siteId);
+
+            return newFile || file;
+        }
+
+        return file;
     }
 
     /**
@@ -240,23 +270,32 @@ export class CorePluginFileDelegate extends CoreDelegate {
      * @param siteId Site ID. If not defined, current site.
      * @return Promise resolved with the size.
      */
-    getFileSize(file: CoreWSExternalFile, siteId?: string): Promise<number> {
+    async getFileSize(file: CoreWSExternalFile, siteId?: string): Promise<number> {
+        const isDownloadable = await this.isFileDownloadable(file, siteId);
+
+        if (!isDownloadable.downloadable) {
+            return 0;
+        }
+
         const handler = this.getHandlerForFile(file);
 
         // First of all check if file can be downloaded.
-        return this.getHandlerDownloadableFile(file, handler, siteId).then((file) => {
-            if (!file) {
-                return 0;
-            }
+        const downloadableFile = await this.getHandlerDownloadableFile(file, handler, siteId);
+        if (!downloadableFile) {
+            return 0;
+        }
 
-            if (handler && handler.getFileSize) {
-                return handler.getFileSize(file, siteId).catch(() => {
-                    return file.filesize;
-                });
-            }
+        if (handler && handler.getFileSize) {
+            try {
+                const size = handler.getFileSize(downloadableFile, siteId);
 
-            return Promise.resolve(file.filesize);
-        });
+                return size;
+            } catch (error) {
+                // Ignore errors.
+            }
+        }
+
+        return downloadableFile.filesize;
     }
 
     /**
@@ -275,6 +314,24 @@ export class CorePluginFileDelegate extends CoreDelegate {
         }
     }
 
+    /**
+     * Check if a file is downloadable.
+     *
+     * @param file The file data.
+     * @param siteId Site ID. If not defined, current site.
+     * @return Promise with the data.
+     */
+    isFileDownloadable(file: CoreWSExternalFile, siteId?: string): Promise<CorePluginFileDownloadableResult> {
+        const handler = this.getHandlerForFile(file);
+
+        if (handler && handler.isFileDownloadable) {
+            return handler.isFileDownloadable(file, siteId);
+        }
+
+        // Default to true.
+        return Promise.resolve({downloadable: true});
+    }
+
     /**
      * Removes the revision number from a file URL.
      *
diff --git a/src/providers/utils/text.ts b/src/providers/utils/text.ts
index 0f1202161..8565b6dfc 100644
--- a/src/providers/utils/text.ts
+++ b/src/providers/utils/text.ts
@@ -18,6 +18,16 @@ import { ModalController, Platform } from 'ionic-angular';
 import { TranslateService } from '@ngx-translate/core';
 import { CoreLangProvider } from '../lang';
 
+/**
+ * Different type of errors the app can treat.
+ */
+export type CoreTextErrorObject = {
+    message?: string;
+    error?: string;
+    content?: string;
+    body?: string;
+};
+
 /*
  * "Utils" service with helper functions for text.
 */
@@ -122,6 +132,38 @@ export class CoreTextUtilsProvider {
         return result;
     }
 
+    /**
+     * Build a message with several paragraphs.
+     *
+     * @param paragraphs List of paragraphs.
+     * @return Built message.
+     */
+    buildSeveralParagraphsMessage(paragraphs: (string | CoreTextErrorObject)[]): string {
+        // Filter invalid messages, and convert them to messages in case they're errors.
+        const messages: string[] = [];
+
+        paragraphs.forEach((paragraph) => {
+            // If it's an error, get its message.
+            const message = this.getErrorMessageFromError(paragraph);
+
+            if (paragraph) {
+                messages.push(message);
+            }
+        });
+
+        if (messages.length < 2) {
+            return messages[0] || '';
+        }
+
+        let builtMessage = messages[0];
+
+        for (let i = 1; i < messages.length; i++) {
+            builtMessage = this.translate.instant('core.twoparagraphs', { p1: builtMessage, p2: messages[i] });
+        }
+
+        return builtMessage;
+    }
+
     /**
      * Convert size in bytes into human readable format
      *
@@ -449,7 +491,7 @@ export class CoreTextUtilsProvider {
      * @param error Error object.
      * @return Error message, undefined if not found.
      */
-    getErrorMessageFromError(error: any): string {
+    getErrorMessageFromError(error: string | CoreTextErrorObject): string {
         if (typeof error == 'string') {
             return error;
         }
diff --git a/src/providers/utils/url.ts b/src/providers/utils/url.ts
index ec233b1c0..5c8c0e35a 100644
--- a/src/providers/utils/url.ts
+++ b/src/providers/utils/url.ts
@@ -469,4 +469,32 @@ export class CoreUrlUtilsProvider {
 
         return matches && matches[0];
     }
+
+    /**
+     * Modifies a pluginfile URL to use the default pluginfile script instead of the webservice one.
+     *
+     * @param url The url to be fixed.
+     * @param siteUrl The URL of the site the URL belongs to.
+     * @return Modified URL.
+     */
+    unfixPluginfileURL(url: string, siteUrl?: string): string {
+        if (!url) {
+            return '';
+        }
+
+        url = url.replace(/&amp;/g, '&');
+
+        // It site URL is supplied, check if the URL belongs to the site.
+        if (siteUrl && url.indexOf(this.textUtils.addEndingSlash(siteUrl)) !== 0) {
+            return url;
+        }
+
+        // Not a pluginfile URL. Treat webservice/pluginfile case.
+        url = url.replace(/\/webservice\/pluginfile\.php\//, '/pluginfile.php/');
+
+        // Make sure the URL doesn't contain the token.
+        url.replace(/([?&])token=[^&]*&?/, '$1');
+
+        return url;
+    }
 }