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';
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<HTMLIFrameElement>();
@ -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));
}
}
}

View File

@ -93,5 +93,6 @@
"statusbarbgremotetheme": "#000000",
"statusbarlighttextremotetheme": true,
"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 { 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<any> {
protected async handleExternalContent(targetAttr: string, url: string, siteId?: string): Promise<any> {
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 = <HTMLElement> 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 = <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);
}
});
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;
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.

View File

@ -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);

View File

@ -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() ? (<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) {}

View File

@ -412,11 +412,21 @@ export class CoreFilepoolProvider {
protected packagesPromises = {};
protected filePromises: { [s: string]: { [s: string]: Promise<any> } } = {};
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());
});
});
}

View File

@ -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 = (<HTMLFrameElement> element).src || (<HTMLObjectElement> element).data,
frameScheme = this.urlUtils.getUrlScheme(frameSrc);
const frameSrc = (<HTMLFrameElement> element).src || (<HTMLObjectElement> 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);

View File

@ -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 '<img src="' + path + '">';

View File

@ -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.
*

View File

@ -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<any> {
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 {