MOBILE-3101 core: Fix embedded files

main
Dani Palou 2020-03-23 10:57:49 +01:00
parent 38e3e88ed6
commit 47decde520
11 changed files with 173 additions and 104 deletions

View File

@ -17,6 +17,7 @@ import {
} from '@angular/core'; } from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { NavController } from 'ionic-angular'; import { NavController } from 'ionic-angular';
import { CoreFileProvider } from '@providers/file';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUrlUtilsProvider } from '@providers/utils/url';
@ -49,7 +50,8 @@ export class CoreIframeComponent implements OnInit, OnChanges {
protected navCtrl: NavController, protected navCtrl: NavController,
protected urlUtils: CoreUrlUtilsProvider, protected urlUtils: CoreUrlUtilsProvider,
protected utils: CoreUtilsProvider, protected utils: CoreUtilsProvider,
@Optional() protected svComponent: CoreSplitViewComponent) { @Optional() protected svComponent: CoreSplitViewComponent,
protected fileProvider: CoreFileProvider) {
this.logger = logger.getInstance('CoreIframe'); this.logger = logger.getInstance('CoreIframe');
this.loaded = new EventEmitter<HTMLIFrameElement>(); this.loaded = new EventEmitter<HTMLIFrameElement>();
@ -93,8 +95,8 @@ export class CoreIframeComponent implements OnInit, OnChanges {
*/ */
ngOnChanges(changes: {[name: string]: SimpleChange }): void { ngOnChanges(changes: {[name: string]: SimpleChange }): void {
if (changes.src) { if (changes.src) {
const youtubeUrl = this.urlUtils.getYoutubeEmbedUrl(changes.src.currentValue); const url = this.urlUtils.getYoutubeEmbedUrl(changes.src.currentValue) || changes.src.currentValue;
this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(youtubeUrl || changes.src.currentValue); this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.fileProvider.convertFileSrc(url));
} }
} }
} }

View File

@ -93,5 +93,6 @@
"statusbarbgremotetheme": "#000000", "statusbarbgremotetheme": "#000000",
"statusbarlighttextremotetheme": true, "statusbarlighttextremotetheme": true,
"enableanalytics": false, "enableanalytics": false,
"forceColorScheme": "" "forceColorScheme": "",
"webviewscheme": "moodleappfs"
} }

View File

@ -16,6 +16,7 @@ import { Directive, Input, AfterViewInit, ElementRef, OnChanges, SimpleChange, O
import { Platform } from 'ionic-angular'; import { Platform } from 'ionic-angular';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreFile } from '@providers/file';
import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreFilepoolProvider } from '@providers/filepool';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
@ -52,9 +53,15 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
invalid = false; invalid = false;
constructor(element: ElementRef, logger: CoreLoggerProvider, private filepoolProvider: CoreFilepoolProvider, constructor(element: ElementRef,
private platform: Platform, private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider, logger: CoreLoggerProvider,
private urlUtils: CoreUrlUtilsProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider) { 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 directive can be added dynamically. In that case, the first param is the HTMLElement.
this.element = element.nativeElement || element; this.element = element.nativeElement || element;
this.logger = logger.getInstance('CoreExternalContentDirective'); this.logger = logger.getInstance('CoreExternalContentDirective');
@ -179,7 +186,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
* @param siteId Site ID. * @param siteId Site ID.
* @return Promise resolved if the element is successfully treated. * @return Promise resolved if the element is successfully treated.
*/ */
protected handleExternalContent(targetAttr: string, url: string, siteId?: string): Promise<any> { protected async handleExternalContent(targetAttr: string, url: string, siteId?: string): Promise<any> {
const tagName = this.element.tagName; const tagName = this.element.tagName;
@ -214,72 +221,70 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
this.addSource(url); this.addSource(url);
} }
return Promise.reject(null); throw 'Non-downloadable URL';
} }
// Get the webservice pluginfile URL, we ignore failures here. const site = await this.sitesProvider.getSite(siteId);
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.
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 = <HTMLElement> this.domUtils.closest(this.element, 'video,audio');
if (!clickableEl) {
return;
}
} }
// Download images, tracks and posters if size is unknown. clickableEl.addEventListener(eventName, () => {
const dwnUnknown = tagName == 'IMG' || tagName == 'TRACK' || targetAttr == 'poster'; // User played media or opened a downloadable link.
let promise; // Download the file if in wifi and it hasn't been downloaded already (for big files).
if (this.appProvider.isWifi()) {
if (targetAttr === 'src' && tagName !== 'SOURCE' && tagName !== 'TRACK' && tagName !== 'VIDEO' && // We aren't using the result, so it doesn't matter which of the 2 functions we call.
tagName !== 'AUDIO') { this.filepoolProvider.getUrlByUrl(siteId, url, this.component, this.componentId, 0, false);
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 = <HTMLElement> 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);
}
});
} }
}); });
}); }
} }
/** /**

View File

@ -74,26 +74,27 @@ export class CoreFormatTextDirective implements OnChanges {
protected loadingChangedListener; protected loadingChangedListener;
constructor(element: ElementRef, constructor(element: ElementRef,
private sitesProvider: CoreSitesProvider, protected sitesProvider: CoreSitesProvider,
private domUtils: CoreDomUtilsProvider, protected domUtils: CoreDomUtilsProvider,
private textUtils: CoreTextUtilsProvider, protected textUtils: CoreTextUtilsProvider,
private translate: TranslateService, protected translate: TranslateService,
private platform: Platform, protected platform: Platform,
private utils: CoreUtilsProvider, protected utils: CoreUtilsProvider,
private urlUtils: CoreUrlUtilsProvider, protected urlUtils: CoreUrlUtilsProvider,
private loggerProvider: CoreLoggerProvider, protected loggerProvider: CoreLoggerProvider,
private filepoolProvider: CoreFilepoolProvider, protected filepoolProvider: CoreFilepoolProvider,
private appProvider: CoreAppProvider, protected appProvider: CoreAppProvider,
private contentLinksHelper: CoreContentLinksHelperProvider, protected contentLinksHelper: CoreContentLinksHelperProvider,
@Optional() private navCtrl: NavController, @Optional() protected navCtrl: NavController,
@Optional() private content: Content, @Optional() @Optional() protected content: Content, @Optional()
private svComponent: CoreSplitViewComponent, protected svComponent: CoreSplitViewComponent,
private iframeUtils: CoreIframeUtilsProvider, protected iframeUtils: CoreIframeUtilsProvider,
private eventsProvider: CoreEventsProvider, protected eventsProvider: CoreEventsProvider,
private filterProvider: CoreFilterProvider, protected filterProvider: CoreFilterProvider,
private filterHelper: CoreFilterHelperProvider, protected filterHelper: CoreFilterHelperProvider,
private filterDelegate: CoreFilterDelegate, protected filterDelegate: CoreFilterDelegate,
private viewContainerRef: ViewContainerRef) { protected viewContainerRef: ViewContainerRef,
) {
this.element = element.nativeElement; this.element = element.nativeElement;
this.element.classList.add('opacity-hide'); // Hide contents until they're treated. this.element.classList.add('opacity-hide'); // Hide contents until they're treated.

View File

@ -92,7 +92,7 @@ export class CoreLinkDirective implements OnInit {
protected navigate(href: string): void { protected navigate(href: string): void {
const contentLinksScheme = CoreConfigConstants.customurlscheme + '://link='; 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. // We have a local file.
this.utils.openFile(href).catch((error) => { this.utils.openFile(href).catch((error) => {
this.domUtils.showErrorModal(error); this.domUtils.showErrorModal(error);

View File

@ -20,6 +20,7 @@ import { CoreAppProvider } from './app';
import { CoreLoggerProvider } from './logger'; import { CoreLoggerProvider } from './logger';
import { CoreMimetypeUtilsProvider } from './utils/mimetype'; import { CoreMimetypeUtilsProvider } from './utils/mimetype';
import { CoreTextUtilsProvider } from './utils/text'; import { CoreTextUtilsProvider } from './utils/text';
import { CoreConfigConstants } from '../configconstants';
import { Zip } from '@ionic-native/zip'; import { Zip } from '@ionic-native/zip';
import { makeSingleton } from '@singletons/core.singletons'; import { makeSingleton } from '@singletons/core.singletons';
@ -946,6 +947,7 @@ export class CoreFileProvider {
/** /**
* Get the internal URL of a file. * 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. * @param fileEntry File Entry.
* @return Internal URL. * @return Internal URL.
@ -1270,6 +1272,31 @@ export class CoreFileProvider {
return window.location.href; 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() ? (<any> 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) {} export class CoreFile extends makeSingleton(CoreFileProvider) {}

View File

@ -412,11 +412,21 @@ export class CoreFilepoolProvider {
protected packagesPromises = {}; protected packagesPromises = {};
protected filePromises: { [s: string]: { [s: string]: Promise<any> } } = {}; protected filePromises: { [s: string]: { [s: string]: Promise<any> } } = {};
constructor(logger: CoreLoggerProvider, private appProvider: CoreAppProvider, private fileProvider: CoreFileProvider, constructor(logger: CoreLoggerProvider,
private sitesProvider: CoreSitesProvider, private wsProvider: CoreWSProvider, private textUtils: CoreTextUtilsProvider, protected appProvider: CoreAppProvider,
private utils: CoreUtilsProvider, private mimeUtils: CoreMimetypeUtilsProvider, private urlUtils: CoreUrlUtilsProvider, protected fileProvider: CoreFileProvider,
private timeUtils: CoreTimeUtilsProvider, private eventsProvider: CoreEventsProvider, initDelegate: CoreInitDelegate, protected sitesProvider: CoreSitesProvider,
network: Network, private pluginFileDelegate: CorePluginFileDelegate, private domUtils: CoreDomUtilsProvider, 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) { zone: NgZone) {
this.logger = logger.getInstance('CoreFilepoolProvider'); this.logger = logger.getInstance('CoreFilepoolProvider');
@ -1796,8 +1806,7 @@ export class CoreFilepoolProvider {
if (this.fileProvider.isAvailable()) { if (this.fileProvider.isAvailable()) {
return Promise.resolve(this.getFilePath(siteId, fileId)).then((path) => { return Promise.resolve(this.getFilePath(siteId, fileId)).then((path) => {
return this.fileProvider.getFile(path).then((fileEntry) => { return this.fileProvider.getFile(path).then((fileEntry) => {
// We use toInternalURL so images are loaded in iOS8 using img HTML tags. return this.fileProvider.convertFileSrc(fileEntry.toURL());
return this.fileProvider.getInternalURL(fileEntry);
}); });
}); });
} }

View File

@ -225,7 +225,7 @@ export class CoreIframeUtilsProvider {
} else { } else {
element.setAttribute('src', url); 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. // It's a local file.
this.utils.openFile(url).catch((error) => { this.utils.openFile(url).catch((error) => {
this.domUtils.showErrorModal(error); this.domUtils.showErrorModal(error);
@ -353,16 +353,14 @@ export class CoreIframeUtilsProvider {
return; return;
} }
if (scheme && scheme != 'file' && scheme != 'filesystem') { if (!this.urlUtils.isLocalFileUrlScheme(scheme)) {
// Scheme suggests it's an external resource. // Scheme suggests it's an external resource.
event.preventDefault(); event.preventDefault();
const frameSrc = (<HTMLFrameElement> element).src || (<HTMLObjectElement> element).data, const frameSrc = (<HTMLFrameElement> element).src || (<HTMLObjectElement> element).data;
frameScheme = this.urlUtils.getUrlScheme(frameSrc);
// If the frame is not local, check the target to identify how to treat the link. // If the frame is not local, check the target to identify how to treat the link.
if (frameScheme && frameScheme != 'file' && frameScheme != 'filesystem' && if (!this.urlUtils.isLocalFileUrl(frameSrc) && (!link.target || link.target == '_self')) {
(!link.target || link.target == '_self')) {
// Load the link inside the frame itself. // Load the link inside the frame itself.
if (element.tagName.toLowerCase() == 'object') { if (element.tagName.toLowerCase() == 'object') {
element.setAttribute('data', link.href); element.setAttribute('data', link.href);

View File

@ -14,6 +14,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { CoreFile } from '../file';
import { CoreLoggerProvider } from '../logger'; import { CoreLoggerProvider } from '../logger';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreTextUtilsProvider } from './text'; import { CoreTextUtilsProvider } from './text';
@ -165,7 +166,7 @@ export class CoreMimetypeUtilsProvider {
if (this.canBeEmbedded(ext)) { if (this.canBeEmbedded(ext)) {
file.embedType = this.getExtensionType(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') { if (file.embedType == 'image') {
return '<img src="' + path + '">'; return '<img src="' + path + '">';

View File

@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
import { CoreLangProvider } from '../lang'; import { CoreLangProvider } from '../lang';
import { CoreTextUtilsProvider } from './text'; import { CoreTextUtilsProvider } from './text';
import { makeSingleton } from '@singletons/core.singletons'; import { makeSingleton } from '@singletons/core.singletons';
import { CoreConfigConstants } from '../../configconstants';
/* /*
* "Utils" service with helper functions for URLs. * "Utils" service with helper functions for URLs.
@ -424,6 +425,26 @@ export class CoreUrlUtilsProvider {
return /^https?\:\/\/.+/i.test(url); 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. * Returns if a URL is a pluginfile URL.
* *

View File

@ -27,6 +27,7 @@ import { CoreLoggerProvider } from '../logger';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreLangProvider } from '../lang'; import { CoreLangProvider } from '../lang';
import { CoreWSProvider, CoreWSError } from '../ws'; import { CoreWSProvider, CoreWSError } from '../ws';
import { CoreFile } from '../file';
import { makeSingleton } from '@singletons/core.singletons'; import { makeSingleton } from '@singletons/core.singletons';
/** /**
@ -863,8 +864,11 @@ export class CoreUtilsProvider {
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
openFile(path: string): Promise<any> { openFile(path: string): Promise<any> {
const extension = this.mimetypeUtils.getFileExtension(path), // Convert the path to a native path if needed.
mimetype = this.mimetypeUtils.getMimeType(extension); 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. // Path needs to be decoded, the file won't be opened if the path has %20 instead of spaces and so.
try { try {