diff --git a/src/components/iframe/iframe.ts b/src/components/iframe/iframe.ts index 516012a8f..5e98b10f7 100644 --- a/src/components/iframe/iframe.ts +++ b/src/components/iframe/iframe.ts @@ -17,6 +17,7 @@ import { } from '@angular/core'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { NavController } from 'ionic-angular'; +import { CoreFileProvider } from '@providers/file'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; @@ -49,7 +50,8 @@ export class CoreIframeComponent implements OnInit, OnChanges { protected navCtrl: NavController, protected urlUtils: CoreUrlUtilsProvider, protected utils: CoreUtilsProvider, - @Optional() protected svComponent: CoreSplitViewComponent) { + @Optional() protected svComponent: CoreSplitViewComponent, + protected fileProvider: CoreFileProvider) { this.logger = logger.getInstance('CoreIframe'); this.loaded = new EventEmitter(); @@ -93,8 +95,8 @@ export class CoreIframeComponent implements OnInit, OnChanges { */ ngOnChanges(changes: {[name: string]: SimpleChange }): void { if (changes.src) { - const youtubeUrl = this.urlUtils.getYoutubeEmbedUrl(changes.src.currentValue); - this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(youtubeUrl || changes.src.currentValue); + const url = this.urlUtils.getYoutubeEmbedUrl(changes.src.currentValue) || changes.src.currentValue; + this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.fileProvider.convertFileSrc(url)); } } } diff --git a/src/config.json b/src/config.json index 50897768a..a81136800 100644 --- a/src/config.json +++ b/src/config.json @@ -93,5 +93,6 @@ "statusbarbgremotetheme": "#000000", "statusbarlighttextremotetheme": true, "enableanalytics": false, - "forceColorScheme": "" + "forceColorScheme": "", + "webviewscheme": "moodleappfs" } \ No newline at end of file diff --git a/src/directives/external-content.ts b/src/directives/external-content.ts index 1879772c7..47b4c776e 100644 --- a/src/directives/external-content.ts +++ b/src/directives/external-content.ts @@ -16,6 +16,7 @@ import { Directive, Input, AfterViewInit, ElementRef, OnChanges, SimpleChange, O import { Platform } from 'ionic-angular'; import { CoreAppProvider } from '@providers/app'; import { CoreLoggerProvider } from '@providers/logger'; +import { CoreFile } from '@providers/file'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; @@ -52,9 +53,15 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges { invalid = false; - constructor(element: ElementRef, logger: CoreLoggerProvider, private filepoolProvider: CoreFilepoolProvider, - private platform: Platform, private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider, - private urlUtils: CoreUrlUtilsProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider) { + constructor(element: ElementRef, + logger: CoreLoggerProvider, + protected filepoolProvider: CoreFilepoolProvider, + protected platform: Platform, + protected sitesProvider: CoreSitesProvider, + protected domUtils: CoreDomUtilsProvider, + protected urlUtils: CoreUrlUtilsProvider, + protected appProvider: CoreAppProvider, + protected utils: CoreUtilsProvider) { // This directive can be added dynamically. In that case, the first param is the HTMLElement. this.element = element.nativeElement || element; this.logger = logger.getInstance('CoreExternalContentDirective'); @@ -179,7 +186,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges { * @param siteId Site ID. * @return Promise resolved if the element is successfully treated. */ - protected handleExternalContent(targetAttr: string, url: string, siteId?: string): Promise { + protected async handleExternalContent(targetAttr: string, url: string, siteId?: string): Promise { const tagName = this.element.tagName; @@ -214,72 +221,70 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges { this.addSource(url); } - return Promise.reject(null); + throw 'Non-downloadable URL'; } - // Get the webservice pluginfile URL, we ignore failures here. - return this.sitesProvider.getSite(siteId).then((site) => { - if (!site.canDownloadFiles() && this.urlUtils.isPluginFileUrl(url)) { - this.element.parentElement.removeChild(this.element); // Remove element since it'll be broken. + const site = await this.sitesProvider.getSite(siteId); - return Promise.reject(null); + if (!site.canDownloadFiles() && this.urlUtils.isPluginFileUrl(url)) { + this.element.parentElement.removeChild(this.element); // Remove element since it'll be broken. + + throw 'Site doesn\'t allow downloading files.'; + } + + // Download images, tracks and posters if size is unknown. + const dwnUnknown = tagName == 'IMG' || tagName == 'TRACK' || targetAttr == 'poster'; + let finalUrl: string; + + if (targetAttr === 'src' && tagName !== 'SOURCE' && tagName !== 'TRACK' && tagName !== 'VIDEO' && tagName !== 'AUDIO') { + finalUrl = await this.filepoolProvider.getSrcByUrl(siteId, url, this.component, this.componentId, 0, true, dwnUnknown); + } else { + finalUrl = await this.filepoolProvider.getUrlByUrl(siteId, url, this.component, this.componentId, 0, true, dwnUnknown); + + finalUrl = CoreFile.instance.convertFileSrc(finalUrl); + } + + if (finalUrl.match(/^https?:\/\//i)) { + /* In iOS, if we use the same URL in embedded file and background download then the download only + downloads a few bytes (cached ones). Add a hash to the URL so both URLs are different. */ + finalUrl = finalUrl + '#moodlemobile-embedded'; + } + + this.logger.debug('Using URL ' + finalUrl + ' for ' + url); + if (tagName === 'SOURCE') { + // The browser does not catch changes in SRC, we need to add a new source. + this.addSource(finalUrl); + } else { + if (tagName === 'IMG') { + this.loaded = false; + this.waitForLoad(); + } + this.element.setAttribute(targetAttr, finalUrl); + this.element.setAttribute('data-original-' + targetAttr, url); + } + + // Set events to download big files (not downloaded automatically). + if (finalUrl.indexOf('http') === 0 && targetAttr != 'poster' && + (tagName == 'VIDEO' || tagName == 'AUDIO' || tagName == 'A' || tagName == 'SOURCE')) { + const eventName = tagName == 'A' ? 'click' : 'play'; + let clickableEl = this.element; + + if (tagName == 'SOURCE') { + clickableEl = this.domUtils.closest(this.element, 'video,audio'); + if (!clickableEl) { + return; + } } - // Download images, tracks and posters if size is unknown. - const dwnUnknown = tagName == 'IMG' || tagName == 'TRACK' || targetAttr == 'poster'; - let promise; - - if (targetAttr === 'src' && tagName !== 'SOURCE' && tagName !== 'TRACK' && tagName !== 'VIDEO' && - tagName !== 'AUDIO') { - promise = this.filepoolProvider.getSrcByUrl(siteId, url, this.component, this.componentId, 0, true, dwnUnknown); - } else { - promise = this.filepoolProvider.getUrlByUrl(siteId, url, this.component, this.componentId, 0, true, dwnUnknown); - } - - return promise.then((finalUrl) => { - if (finalUrl.match(/^https?:\/\//i)) { - /* In iOS, if we use the same URL in embedded file and background download then the download only - downloads a few bytes (cached ones). Add a hash to the URL so both URLs are different. */ - finalUrl = finalUrl + '#moodlemobile-embedded'; - } - - this.logger.debug('Using URL ' + finalUrl + ' for ' + url); - if (tagName === 'SOURCE') { - // The browser does not catch changes in SRC, we need to add a new source. - this.addSource(finalUrl); - } else { - if (tagName === 'IMG') { - this.loaded = false; - this.waitForLoad(); - } - this.element.setAttribute(targetAttr, finalUrl); - this.element.setAttribute('data-original-' + targetAttr, url); - } - - // Set events to download big files (not downloaded automatically). - if (finalUrl.indexOf('http') === 0 && targetAttr != 'poster' && - (tagName == 'VIDEO' || tagName == 'AUDIO' || tagName == 'A' || tagName == 'SOURCE')) { - const eventName = tagName == 'A' ? 'click' : 'play'; - let clickableEl = this.element; - - if (tagName == 'SOURCE') { - clickableEl = this.domUtils.closest(this.element, 'video,audio'); - if (!clickableEl) { - return; - } - } - - clickableEl.addEventListener(eventName, () => { - // User played media or opened a downloadable link. - // Download the file if in wifi and it hasn't been downloaded already (for big files). - if (this.appProvider.isWifi()) { - // We aren't using the result, so it doesn't matter which of the 2 functions we call. - this.filepoolProvider.getUrlByUrl(siteId, url, this.component, this.componentId, 0, false); - } - }); + clickableEl.addEventListener(eventName, () => { + // User played media or opened a downloadable link. + // Download the file if in wifi and it hasn't been downloaded already (for big files). + if (this.appProvider.isWifi()) { + // We aren't using the result, so it doesn't matter which of the 2 functions we call. + this.filepoolProvider.getUrlByUrl(siteId, url, this.component, this.componentId, 0, false); } }); - }); + } } /** diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index 7057b269f..9dfe8a746 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -74,26 +74,27 @@ export class CoreFormatTextDirective implements OnChanges { protected loadingChangedListener; constructor(element: ElementRef, - private sitesProvider: CoreSitesProvider, - private domUtils: CoreDomUtilsProvider, - private textUtils: CoreTextUtilsProvider, - private translate: TranslateService, - private platform: Platform, - private utils: CoreUtilsProvider, - private urlUtils: CoreUrlUtilsProvider, - private loggerProvider: CoreLoggerProvider, - private filepoolProvider: CoreFilepoolProvider, - private appProvider: CoreAppProvider, - private contentLinksHelper: CoreContentLinksHelperProvider, - @Optional() private navCtrl: NavController, - @Optional() private content: Content, @Optional() - private svComponent: CoreSplitViewComponent, - private iframeUtils: CoreIframeUtilsProvider, - private eventsProvider: CoreEventsProvider, - private filterProvider: CoreFilterProvider, - private filterHelper: CoreFilterHelperProvider, - private filterDelegate: CoreFilterDelegate, - private viewContainerRef: ViewContainerRef) { + protected sitesProvider: CoreSitesProvider, + protected domUtils: CoreDomUtilsProvider, + protected textUtils: CoreTextUtilsProvider, + protected translate: TranslateService, + protected platform: Platform, + protected utils: CoreUtilsProvider, + protected urlUtils: CoreUrlUtilsProvider, + protected loggerProvider: CoreLoggerProvider, + protected filepoolProvider: CoreFilepoolProvider, + protected appProvider: CoreAppProvider, + protected contentLinksHelper: CoreContentLinksHelperProvider, + @Optional() protected navCtrl: NavController, + @Optional() protected content: Content, @Optional() + protected svComponent: CoreSplitViewComponent, + protected iframeUtils: CoreIframeUtilsProvider, + protected eventsProvider: CoreEventsProvider, + protected filterProvider: CoreFilterProvider, + protected filterHelper: CoreFilterHelperProvider, + protected filterDelegate: CoreFilterDelegate, + protected viewContainerRef: ViewContainerRef, + ) { this.element = element.nativeElement; this.element.classList.add('opacity-hide'); // Hide contents until they're treated. diff --git a/src/directives/link.ts b/src/directives/link.ts index 5ccfe3c64..5d85e2199 100644 --- a/src/directives/link.ts +++ b/src/directives/link.ts @@ -92,7 +92,7 @@ export class CoreLinkDirective implements OnInit { protected navigate(href: string): void { const contentLinksScheme = CoreConfigConstants.customurlscheme + '://link='; - if (href.indexOf('cdvfile://') === 0 || href.indexOf('file://') === 0 || href.indexOf('filesystem:') === 0) { + if (this.urlUtils.isLocalFileUrl(href)) { // We have a local file. this.utils.openFile(href).catch((error) => { this.domUtils.showErrorModal(error); diff --git a/src/providers/file.ts b/src/providers/file.ts index 1af76887e..1bee27880 100644 --- a/src/providers/file.ts +++ b/src/providers/file.ts @@ -20,6 +20,7 @@ import { CoreAppProvider } from './app'; import { CoreLoggerProvider } from './logger'; import { CoreMimetypeUtilsProvider } from './utils/mimetype'; import { CoreTextUtilsProvider } from './utils/text'; +import { CoreConfigConstants } from '../configconstants'; import { Zip } from '@ionic-native/zip'; import { makeSingleton } from '@singletons/core.singletons'; @@ -946,6 +947,7 @@ export class CoreFileProvider { /** * Get the internal URL of a file. + * Please notice that with WKWebView these URLs no longer work in mobile. Use fileEntry.toURL() along with convertFileSrc. * * @param fileEntry File Entry. * @return Internal URL. @@ -1270,6 +1272,31 @@ export class CoreFileProvider { return window.location.href; } + + /** + * Helper function to call Ionic WebView convertFileSrc only in the needed platforms. + * This is needed to make files work with the Ionic WebView plugin. + * + * @param src Source to convert. + * @return Converted src. + */ + convertFileSrc(src: string): string { + return this.appProvider.isMobile() ? ( window).Ionic.WebView.convertFileSrc(src) : src; + } + + /** + * Undo the conversion of convertFileSrc. + * + * @param src Source to unconvert. + * @return Unconverted src. + */ + unconvertFileSrc(src: string): string { + if (!this.appProvider.isMobile()) { + return src; + } + + return src.replace(CoreConfigConstants.webviewscheme + '://localhost/_app_file_', 'file://'); + } } export class CoreFile extends makeSingleton(CoreFileProvider) {} diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts index 40a5c0d56..3d4ff5bfe 100644 --- a/src/providers/filepool.ts +++ b/src/providers/filepool.ts @@ -412,11 +412,21 @@ export class CoreFilepoolProvider { protected packagesPromises = {}; protected filePromises: { [s: string]: { [s: string]: Promise } } = {}; - constructor(logger: CoreLoggerProvider, private appProvider: CoreAppProvider, private fileProvider: CoreFileProvider, - private sitesProvider: CoreSitesProvider, private wsProvider: CoreWSProvider, private textUtils: CoreTextUtilsProvider, - private utils: CoreUtilsProvider, private mimeUtils: CoreMimetypeUtilsProvider, private urlUtils: CoreUrlUtilsProvider, - private timeUtils: CoreTimeUtilsProvider, private eventsProvider: CoreEventsProvider, initDelegate: CoreInitDelegate, - network: Network, private pluginFileDelegate: CorePluginFileDelegate, private domUtils: CoreDomUtilsProvider, + constructor(logger: CoreLoggerProvider, + protected appProvider: CoreAppProvider, + protected fileProvider: CoreFileProvider, + protected sitesProvider: CoreSitesProvider, + protected wsProvider: CoreWSProvider, + protected textUtils: CoreTextUtilsProvider, + protected utils: CoreUtilsProvider, + protected mimeUtils: CoreMimetypeUtilsProvider, + protected urlUtils: CoreUrlUtilsProvider, + protected timeUtils: CoreTimeUtilsProvider, + protected eventsProvider: CoreEventsProvider, + initDelegate: CoreInitDelegate, + network: Network, + protected pluginFileDelegate: CorePluginFileDelegate, + protected domUtils: CoreDomUtilsProvider, zone: NgZone) { this.logger = logger.getInstance('CoreFilepoolProvider'); @@ -1796,8 +1806,7 @@ export class CoreFilepoolProvider { if (this.fileProvider.isAvailable()) { return Promise.resolve(this.getFilePath(siteId, fileId)).then((path) => { return this.fileProvider.getFile(path).then((fileEntry) => { - // We use toInternalURL so images are loaded in iOS8 using img HTML tags. - return this.fileProvider.getInternalURL(fileEntry); + return this.fileProvider.convertFileSrc(fileEntry.toURL()); }); }); } diff --git a/src/providers/utils/iframe.ts b/src/providers/utils/iframe.ts index 5b82ebab2..69c4251f8 100644 --- a/src/providers/utils/iframe.ts +++ b/src/providers/utils/iframe.ts @@ -225,7 +225,7 @@ export class CoreIframeUtilsProvider { } else { element.setAttribute('src', url); } - } else if (url.indexOf('cdvfile://') === 0 || url.indexOf('file://') === 0) { + } else if (this.urlUtils.isLocalFileUrl(url)) { // It's a local file. this.utils.openFile(url).catch((error) => { this.domUtils.showErrorModal(error); @@ -353,16 +353,14 @@ export class CoreIframeUtilsProvider { return; } - if (scheme && scheme != 'file' && scheme != 'filesystem') { + if (!this.urlUtils.isLocalFileUrlScheme(scheme)) { // Scheme suggests it's an external resource. event.preventDefault(); - const frameSrc = ( element).src || ( element).data, - frameScheme = this.urlUtils.getUrlScheme(frameSrc); + const frameSrc = ( element).src || ( element).data; // If the frame is not local, check the target to identify how to treat the link. - if (frameScheme && frameScheme != 'file' && frameScheme != 'filesystem' && - (!link.target || link.target == '_self')) { + if (!this.urlUtils.isLocalFileUrl(frameSrc) && (!link.target || link.target == '_self')) { // Load the link inside the frame itself. if (element.tagName.toLowerCase() == 'object') { element.setAttribute('data', link.href); diff --git a/src/providers/utils/mimetype.ts b/src/providers/utils/mimetype.ts index 4b256196c..eae580a46 100644 --- a/src/providers/utils/mimetype.ts +++ b/src/providers/utils/mimetype.ts @@ -14,6 +14,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; +import { CoreFile } from '../file'; import { CoreLoggerProvider } from '../logger'; import { TranslateService } from '@ngx-translate/core'; import { CoreTextUtilsProvider } from './text'; @@ -165,7 +166,7 @@ export class CoreMimetypeUtilsProvider { if (this.canBeEmbedded(ext)) { file.embedType = this.getExtensionType(ext); - path = path || file.fileurl || (file.toURL && file.toURL()); + path = CoreFile.instance.convertFileSrc(path || file.fileurl || (file.toURL && file.toURL())); if (file.embedType == 'image') { return ''; diff --git a/src/providers/utils/url.ts b/src/providers/utils/url.ts index 26e2bb691..f9b07929b 100644 --- a/src/providers/utils/url.ts +++ b/src/providers/utils/url.ts @@ -16,6 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreLangProvider } from '../lang'; import { CoreTextUtilsProvider } from './text'; import { makeSingleton } from '@singletons/core.singletons'; +import { CoreConfigConstants } from '../../configconstants'; /* * "Utils" service with helper functions for URLs. @@ -424,6 +425,26 @@ export class CoreUrlUtilsProvider { return /^https?\:\/\/.+/i.test(url); } + /** + * Check whether an URL belongs to a local file. + * + * @param url URL to check. + * @return Whether the URL belongs to a local file. + */ + isLocalFileUrl(url: string): boolean { + return this.isLocalFileUrlScheme(this.getUrlScheme(url)); + } + + /** + * Check whether a URL scheme belongs to a local file. + * + * @param scheme Scheme to check. + * @return Whether the scheme belongs to a local file. + */ + isLocalFileUrlScheme(scheme: string): boolean { + return scheme == 'cdvfile' || scheme == 'file' || scheme == 'filesystem' || scheme == CoreConfigConstants.webviewscheme; + } + /** * Returns if a URL is a pluginfile URL. * diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index f1235e375..9ce7d0ea8 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -27,6 +27,7 @@ import { CoreLoggerProvider } from '../logger'; import { TranslateService } from '@ngx-translate/core'; import { CoreLangProvider } from '../lang'; import { CoreWSProvider, CoreWSError } from '../ws'; +import { CoreFile } from '../file'; import { makeSingleton } from '@singletons/core.singletons'; /** @@ -863,8 +864,11 @@ export class CoreUtilsProvider { * @return Promise resolved when done. */ openFile(path: string): Promise { - const extension = this.mimetypeUtils.getFileExtension(path), - mimetype = this.mimetypeUtils.getMimeType(extension); + // Convert the path to a native path if needed. + path = CoreFile.instance.unconvertFileSrc(path); + + const extension = this.mimetypeUtils.getFileExtension(path); + const mimetype = this.mimetypeUtils.getMimeType(extension); // Path needs to be decoded, the file won't be opened if the path has %20 instead of spaces and so. try {