Skip to content

Commit 0de0394

Browse files
authored
Merge pull request #3915 from dpalou/MOBILE-4173
Mobile 4173
2 parents 40c1169 + 22a8f0d commit 0de0394

File tree

11 files changed

+221
-14
lines changed

11 files changed

+221
-14
lines changed
13 KB
Binary file not shown.

scripts/langindex.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2231,6 +2231,7 @@
22312231
"core.ok": "moodle",
22322232
"core.online": "message",
22332233
"core.openfile": "local_moodlemobileapp",
2234+
"core.openfilewithextension": "local_moodlemobileapp",
22342235
"core.openfullimage": "local_moodlemobileapp",
22352236
"core.openinbrowser": "local_moodlemobileapp",
22362237
"core.openinbrowserdescription": "local_moodlemobileapp",

src/core/classes/element-controllers/FrameElementController.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ import { ElementController } from './ElementController';
1616

1717
/**
1818
* Possible types of frame elements.
19-
*
20-
* @todo Remove frame TAG support.
2119
*/
2220
export type FrameElement = HTMLIFrameElement | HTMLObjectElement | HTMLEmbedElement;
2321

src/core/components/iframe/core-iframe.html

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<!-- Display a loading until the iframe page is loaded. -->
2-
<core-loading [hideUntil]="!loading && safeUrl" />
2+
<core-loading [hideUntil]="!loading && (safeUrl || launchExternalLabel)" />
33

44
<!--The iframe needs to be outside of core-loading, otherwise the request is canceled while loading. -->
55
<!-- Don't add the iframe until safeUrl is set, adding an iframe with null as src causes the iframe to load the whole app. -->
6-
<ng-container *ngIf="safeUrl">
6+
<ng-container *ngIf="safeUrl && !launchExternalLabel">
77
<core-navbar-buttons slot="end" prepend *ngIf="initialized && showFullscreenOnToolbar && !loading">
88
<ion-button fill="clear" (click)="toggleFullscreen()"
99
[attr.aria-label]="(fullscreen ? 'core.disablefullscreen' : 'core.fullscreen') | translate">
@@ -28,3 +28,8 @@
2828
{{ 'core.iframehelp' | translate }}
2929
</ion-button>
3030
</ng-container>
31+
32+
<!-- Iframe content needs to be launched in an external app. -->
33+
<ion-button *ngIf="launchExternalLabel" expand="block" class="ion-text-wrap ion-margin" (click)="launchExternal()">
34+
{{ launchExternalLabel }}
35+
</ion-button>

src/core/components/iframe/iframe.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
5555
safeUrl?: SafeResourceUrl;
5656
displayHelp = false;
5757
fullscreen = false;
58+
launchExternalLabel?: string; // Text to set to the button to launch external app.
5859

5960
initialized = false;
6061

@@ -173,6 +174,19 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
173174

174175
let url = this.src;
175176

177+
if (url) {
178+
const { launchExternal, label } = CoreIframeUtils.frameShouldLaunchExternal(url);
179+
180+
if (launchExternal) {
181+
this.launchExternalLabel = label;
182+
this.loading = false;
183+
184+
return;
185+
}
186+
}
187+
188+
this.launchExternalLabel = undefined;
189+
176190
if (url && !CoreUrlUtils.isLocalFileUrl(url)) {
177191
url = CoreUrlUtils.getYoutubeEmbedUrl(url) || url;
178192
this.displayHelp = CoreIframeUtils.shouldDisplayHelpForUrl(url);
@@ -260,4 +274,17 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
260274
}
261275
}
262276

277+
/**
278+
* Launch content in an external app.
279+
*/
280+
launchExternal(): void {
281+
if (!this.src) {
282+
return;
283+
}
284+
285+
CoreIframeUtils.frameLaunchExternal(this.src, {
286+
site: CoreSites.getCurrentSite(),
287+
});
288+
}
289+
263290
}

src/core/directives/format-text.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -523,10 +523,15 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
523523
});
524524

525525
const iframeControllers = iframes.map(iframe => {
526+
const { launchExternal, label } = CoreIframeUtils.frameShouldLaunchExternal(iframe);
527+
if (launchExternal && this.replaceFrameWithButton(iframe, site, label)) {
528+
return;
529+
}
530+
526531
promises.push(this.treatIframe(iframe, site));
527532

528533
return new FrameElementController(iframe, !this.disabled);
529-
});
534+
}).filter((controller): controller is FrameElementController => controller !== undefined);
530535

531536
svgImages.forEach((image) => {
532537
this.addExternalContent(image);
@@ -562,11 +567,16 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
562567
});
563568

564569
// Handle all kind of frames.
565-
const frameControllers = frames.map<FrameElementController>((frame) => {
570+
const frameControllers = frames.map((frame) => {
571+
const { launchExternal, label } = CoreIframeUtils.frameShouldLaunchExternal(frame);
572+
if (launchExternal && this.replaceFrameWithButton(frame, site, label)) {
573+
return;
574+
}
575+
566576
CoreIframeUtils.treatFrame(frame, false);
567577

568578
return new FrameElementController(frame, !this.disabled);
569-
});
579+
}).filter((controller): controller is FrameElementController => controller !== undefined);
570580

571581
CoreDomUtils.handleBootstrapTooltips(div);
572582

@@ -863,6 +873,38 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
863873
CoreIframeUtils.treatFrame(iframe, false);
864874
}
865875

876+
/**
877+
* Replace a frame with a button to open the frame's URL in an external app.
878+
*
879+
* @param frame Frame element to replace.
880+
* @param site Site instance.
881+
* @param label The text to put in the button.
882+
* @returns Whether iframe was replaced.
883+
*/
884+
protected replaceFrameWithButton(frame: FrameElement, site: CoreSite | undefined, label: string): boolean {
885+
const url = 'src' in frame ? frame.src : frame.data;
886+
if (!url) {
887+
return false;
888+
}
889+
890+
const button = document.createElement('ion-button');
891+
button.setAttribute('expand', 'block');
892+
button.classList.add('ion-text-wrap');
893+
button.innerHTML = label;
894+
895+
button.addEventListener('click', () => {
896+
CoreIframeUtils.frameLaunchExternal(url, {
897+
site,
898+
component: this.component,
899+
componentId: this.componentId,
900+
});
901+
});
902+
903+
frame.replaceWith(button);
904+
905+
return true;
906+
}
907+
866908
/**
867909
* Add iframe help option.
868910
*

src/core/lang.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@
233233
"ok": "OK",
234234
"online": "Online",
235235
"openfile": "Open file",
236+
"openfilewithextension": "Open {{extension}} file",
236237
"openfullimage": "Click here to display the full size image",
237238
"openinbrowser": "Open in browser",
238239
"openinbrowserdescription": "You will be taken to a web browser",

src/core/services/utils/dom.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,7 @@ export class CoreDomUtilsProvider {
687687
const element = this.convertToElement(html);
688688

689689
// Treat elements with src (img, audio, video, ...).
690-
const media = Array.from(element.querySelectorAll<HTMLElement>('img, video, audio, source, track'));
690+
const media = Array.from(element.querySelectorAll<HTMLElement>('img, video, audio, source, track, iframe, embed'));
691691
media.forEach((media: HTMLElement) => {
692692
const currentSrc = media.getAttribute('src');
693693
const newSrc = currentSrc ?

src/core/services/utils/iframe.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ import { CorePath } from '@singletons/path';
3333
import { CorePromisedValue } from '@classes/promised-value';
3434
import { CorePlatform } from '@services/platform';
3535
import { FrameElement } from '@classes/element-controllers/FrameElementController';
36+
import { CoreMimetypeUtils } from './mimetype';
37+
import { CoreFilepool } from '@services/filepool';
38+
import { CoreSite } from '@classes/sites/site';
3639

3740
type CoreFrameElement = FrameElement & {
3841
window?: Window;
@@ -45,7 +48,7 @@ type CoreFrameElement = FrameElement & {
4548
@Injectable({ providedIn: 'root' })
4649
export class CoreIframeUtilsProvider {
4750

48-
static readonly FRAME_TAGS = ['iframe', 'frame', 'object', 'embed'];
51+
static readonly FRAME_TAGS = ['iframe', 'object', 'embed'];
4952

5053
protected logger: CoreLogger;
5154
protected waitAutoLoginDefer?: CorePromisedValue<void>;
@@ -631,6 +634,84 @@ export class CoreIframeUtilsProvider {
631634
});
632635
}
633636

637+
/**
638+
* Check if a frame content should be opened with an external app (PDF reader, browser, etc.).
639+
*
640+
* @param urlOrFrame Either a URL of a frame, or the frame to check.
641+
* @returns Whether it should be opened with an external app, and the label for the action to launch in external.
642+
*/
643+
frameShouldLaunchExternal(urlOrFrame: string | FrameElement): { launchExternal: boolean; label: string } {
644+
const url = typeof urlOrFrame === 'string' ?
645+
urlOrFrame :
646+
('src' in urlOrFrame ? urlOrFrame.src : urlOrFrame.data);
647+
const frame = typeof urlOrFrame !== 'string' && urlOrFrame;
648+
649+
const extension = url && CoreMimetypeUtils.guessExtensionFromUrl(url);
650+
const launchExternal = extension === 'pdf' || (frame && frame.getAttribute('data-open-external') === 'true');
651+
652+
let label = '';
653+
if (launchExternal) {
654+
const mimetype = extension && CoreMimetypeUtils.getMimeType(extension);
655+
656+
label = mimetype && mimetype !== 'text/html' && mimetype !== 'text/plain' ?
657+
Translate.instant('core.openfilewithextension', { extension: extension.toUpperCase() }) :
658+
Translate.instant('core.openinbrowser');
659+
}
660+
661+
return {
662+
launchExternal,
663+
label,
664+
};
665+
}
666+
667+
/**
668+
* Launch a frame content in an external app.
669+
*
670+
* @param url Frame URL.
671+
* @param options Options
672+
*/
673+
async frameLaunchExternal(url: string, options: LaunchExternalOptions = {}): Promise<void> {
674+
const modal = await CoreDomUtils.showModalLoading();
675+
676+
try {
677+
if (!CoreNetwork.isOnline()) {
678+
// User is offline, try to open a local copy of the file if present.
679+
const localUrl = options.site ?
680+
await CoreUtils.ignoreErrors(CoreFilepool.getInternalUrlByUrl(options.site.getId(), url)) :
681+
undefined;
682+
683+
if (localUrl) {
684+
CoreUtils.openFile(localUrl);
685+
} else {
686+
CoreDomUtils.showErrorModal('core.networkerrormsg', true);
687+
}
688+
689+
return;
690+
}
691+
692+
const mimetype = await CoreUtils.ignoreErrors(CoreUtils.getMimeTypeFromUrl(url));
693+
694+
if (!mimetype || mimetype === 'text/html' || mimetype === 'text/plain') {
695+
// It's probably a web page, open in browser.
696+
options.site ? options.site.openInBrowserWithAutoLogin(url) : CoreUtils.openInBrowser(url);
697+
698+
return;
699+
}
700+
701+
// Open the file using the online URL and try to download it in background for offline usage.
702+
if (options.site) {
703+
CoreFilepool.getUrlByUrl(options.site.getId(), url, options.component, options.componentId, 0, false);
704+
705+
url = await options.site.checkAndFixPluginfileURL(url);
706+
}
707+
708+
CoreUtils.openOnlineFile(url);
709+
710+
} finally {
711+
modal.dismiss();
712+
}
713+
}
714+
634715
}
635716

636717
export const CoreIframeUtils = makeSingleton(CoreIframeUtilsProvider);
@@ -641,3 +722,12 @@ export const CoreIframeUtils = makeSingleton(CoreIframeUtilsProvider);
641722
type CoreIframeHTMLAnchorElement = HTMLAnchorElement & {
642723
treated?: boolean; // Whether the element has been treated already.
643724
};
725+
726+
/**
727+
* Options to pass to frameLaunchExternal.
728+
*/
729+
type LaunchExternalOptions = {
730+
site?: CoreSite; // Site the frame belongs to.
731+
component?: string; // Component to download the file if needed.
732+
componentId?: string | number; // Component ID to use in conjunction with the component.
733+
};

src/core/services/utils/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,6 +1230,8 @@ export class CoreUtilsProvider {
12301230
type: CoreAnalyticsEventType.OPEN_LINK,
12311231
link: CoreUrlUtils.unfixPluginfileURL(url),
12321232
});
1233+
1234+
return;
12331235
} catch (error) {
12341236
this.logger.error('Error opening online file ' + url + ' with mimetype ' + mimetype);
12351237
this.logger.error('Error: ', JSON.stringify(error));

src/tests/behat/open_files.feature

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ Feature: It opens files properly.
1111
And the following "course enrolments" exist:
1212
| user | course | role |
1313
| student1 | C1 | student |
14-
And the following "activities" exist:
14+
15+
@lms_from3.10
16+
Scenario: Open a file
17+
Given the following "activities" exist:
1518
| activity | name | intro | display | course | defaultfilename |
1619
| resource | Test TXT | Test TXT description | 5 | C1 | A txt.txt |
1720
| resource | Test RTF | Test RTF description | 5 | C1 | A rtf.rtf |
1821
| resource | Test DOC | Test DOC description | 5 | C1 | A doc.doc |
1922
And the following config values are set as admin:
2023
| filetypeexclusionlist | rtf,doc | tool_mobile |
21-
22-
@lms_from3.10
23-
Scenario: Open a file
24-
Given I entered the resource activity "Test TXT" on course "Course 1" as "student1" in the app
24+
And I entered the resource activity "Test TXT" on course "Course 1" as "student1" in the app
2525
When I press "Open" in the app
2626
Then the app should have opened a browser tab with url "^blob:"
2727

@@ -57,3 +57,44 @@ Feature: It opens files properly.
5757
And I press "Test DOC" in the app
5858
And I press "Open" in the app
5959
Then I should find "This file may not work as expected on this device" in the app
60+
61+
Scenario: Open a PDF embedded using an iframe
62+
# Using http://webserver directly because $WWWROOT cannot be used in generators or when creating the page manually.
63+
Given the following "activities" exist:
64+
| activity | idnumber | course | name | content |
65+
| page | page1 | C1 | Page with embedded PDF | <iframe src="http://webserver/local/moodleappbehat/fixtures/dummy.pdf" width="100%" height="500"></iframe> |
66+
| page | page2 | C1 | Page with embedded web | <iframe src="https://moodle.org" width="100%" height="500" data-open-external="true"></iframe> |
67+
And the following config values are set as admin:
68+
| custommenuitems | PDF item\|http://webserver/local/moodleappbehat/fixtures/dummy.pdf\|embedded | tool_mobile |
69+
And I entered the course "Course 1" as "student1" in the app
70+
71+
When I press "Page with embedded PDF" in the app
72+
And I press "Open PDF file" in the app
73+
Then the app should have opened a browser tab with url "dummy.pdf"
74+
75+
When I close the browser tab opened by the app
76+
And I press the back button in the app
77+
And I press "Page with embedded web" in the app
78+
And I press "Open in browser" in the app
79+
And I press "OK" in the app
80+
Then the app should have opened a browser tab with url "moodle.org"
81+
82+
When I close the browser tab opened by the app
83+
And I switch network connection to offline
84+
And I press "Open in browser" in the app
85+
Then I should find "There was a problem connecting to the site. Please check your connection and try again." in the app
86+
87+
When I press "OK" in the app
88+
And I press the back button in the app
89+
And I press "Page with embedded PDF" in the app
90+
And I press "Open PDF file" in the app
91+
Then the app should have opened a browser tab with url "^blob"
92+
93+
When I close the browser tab opened by the app
94+
When I switch network connection to wifi
95+
And I press the back button in the app
96+
And I press the back button in the app
97+
And I press "More" in the app
98+
And I press "PDF item" in the app
99+
And I press "Open PDF file" in the app
100+
Then the app should have opened a browser tab with url "dummy.pdf"

0 commit comments

Comments
 (0)