Skip to content

Commit 678f2e6

Browse files
committed
MOBILE-3401 ios: Fix iframe links and window.open in iOS
1 parent c6dfa40 commit 678f2e6

File tree

6 files changed

+347
-62
lines changed

6 files changed

+347
-62
lines changed

src/addon/mod/h5pactivity/components/index/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
407407
* @return Whether it's an XAPI post statement of the current activity.
408408
*/
409409
protected isCurrentXAPIPost(data: any): boolean {
410-
if (data.context != 'moodleapp' || data.action != 'xapi_post_statement' || !data.statements) {
410+
if (data.environment != 'moodleapp' || data.context != 'h5p' || data.action != 'xapi_post_statement' || !data.statements) {
411411
return false;
412412
}
413413

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// (C) Copyright 2015 Moodle Pty Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
(function () {
16+
var url = location.href;
17+
18+
if (url.match(/^moodleappfs:\/\/localhost/i) || !url.match(/^[a-z0-9]+:\/\//i)) {
19+
// Same domain as the app, stop.
20+
return;
21+
}
22+
23+
// Redefine window.open.
24+
window.open = function(url, name, specs) {
25+
if (name == '_self') {
26+
// Link should be loaded in the same frame.
27+
location.href = toAbsolute(url);
28+
29+
return;
30+
}
31+
32+
getRootWindow(window).postMessage({
33+
environment: 'moodleapp',
34+
context: 'iframe',
35+
action: 'window_open',
36+
frameUrl: location.href,
37+
url: url,
38+
name: name,
39+
specs: specs,
40+
}, '*');
41+
};
42+
43+
// Handle link clicks.
44+
document.addEventListener('click', (event) => {
45+
if (event.defaultPrevented) {
46+
// Event already prevented by some other code.
47+
return;
48+
}
49+
50+
// Find the link being clicked.
51+
var el = event.target;
52+
while (el && el.tagName !== 'A') {
53+
el = el.parentElement;
54+
}
55+
56+
if (!el || el.treated) {
57+
return;
58+
}
59+
60+
// Add click listener to the link, this way if the iframe has added a listener to the link it will be executed first.
61+
el.treated = true;
62+
el.addEventListener('click', function(event) {
63+
linkClicked(el, event);
64+
});
65+
}, {
66+
capture: true // Use capture to fix this listener not called if the element clicked is too deep in the DOM.
67+
});
68+
69+
70+
71+
/**
72+
* Concatenate two paths, adding a slash between them if needed.
73+
*
74+
* @param leftPath Left path.
75+
* @param rightPath Right path.
76+
* @return Concatenated path.
77+
*/
78+
function concatenatePaths(leftPath, rightPath) {
79+
if (!leftPath) {
80+
return rightPath;
81+
} else if (!rightPath) {
82+
return leftPath;
83+
}
84+
85+
var lastCharLeft = leftPath.slice(-1);
86+
var firstCharRight = rightPath.charAt(0);
87+
88+
if (lastCharLeft === '/' && firstCharRight === '/') {
89+
return leftPath + rightPath.substr(1);
90+
} else if (lastCharLeft !== '/' && firstCharRight !== '/') {
91+
return leftPath + '/' + rightPath;
92+
} else {
93+
return leftPath + rightPath;
94+
}
95+
}
96+
97+
/**
98+
* Get the root window.
99+
*
100+
* @param win Current window to check.
101+
* @return Root window.
102+
*/
103+
function getRootWindow(win) {
104+
if (win.parent === win) {
105+
return win;
106+
}
107+
108+
return getRootWindow(win.parent);
109+
}
110+
111+
/**
112+
* Get the scheme from a URL.
113+
*
114+
* @param url URL to treat.
115+
* @return Scheme, undefined if no scheme found.
116+
*/
117+
function getUrlScheme(url) {
118+
if (!url) {
119+
return;
120+
}
121+
122+
var matches = url.match(/^([a-z][a-z0-9+\-.]*):/);
123+
if (matches && matches[1]) {
124+
return matches[1];
125+
}
126+
}
127+
128+
/**
129+
* Check if a URL is absolute.
130+
*
131+
* @param url URL to treat.
132+
* @return Whether it's absolute.
133+
*/
134+
function isAbsoluteUrl(url) {
135+
return /^[^:]{2,}:\/\//i.test(url);
136+
}
137+
138+
/**
139+
* Check whether a URL scheme belongs to a local file.
140+
*
141+
* @param scheme Scheme to check.
142+
* @return Whether the scheme belongs to a local file.
143+
*/
144+
function isLocalFileUrlScheme(scheme) {
145+
if (scheme) {
146+
scheme = scheme.toLowerCase();
147+
}
148+
149+
return scheme == 'cdvfile' ||
150+
scheme == 'file' ||
151+
scheme == 'filesystem' ||
152+
scheme == 'moodleappfs';
153+
}
154+
155+
/**
156+
* Handle a click on an anchor element.
157+
*
158+
* @param link Anchor element clicked.
159+
* @param event Click event.
160+
*/
161+
function linkClicked(link, event) {
162+
if (event.defaultPrevented) {
163+
// Event already prevented by some other code.
164+
return;
165+
}
166+
167+
var linkScheme = getUrlScheme(link.href);
168+
var pageScheme = getUrlScheme(location.href);
169+
var isTargetSelf = !link.target || link.target == '_self';
170+
171+
if (!link.href || linkScheme == 'javascript') {
172+
// Links with no URL and Javascript links are ignored.
173+
return;
174+
}
175+
176+
event.preventDefault();
177+
178+
if (isTargetSelf && (isLocalFileUrlScheme(linkScheme) || !isLocalFileUrlScheme(pageScheme))) {
179+
// Link should be loaded in the same frame. Don't do it if link is online and frame is local.
180+
location.href = toAbsolute(link.href);
181+
182+
return;
183+
}
184+
185+
getRootWindow(window).postMessage({
186+
environment: 'moodleapp',
187+
context: 'iframe',
188+
action: 'link_clicked',
189+
frameUrl: location.href,
190+
link: {href: link.href, target: link.target},
191+
}, '*');
192+
}
193+
194+
/**
195+
* Convert a URL to an absolute URL if needed using the frame src.
196+
*
197+
* @param url URL to convert.
198+
* @return Absolute URL.
199+
*/
200+
function toAbsolute(url) {
201+
if (isAbsoluteUrl(url)) {
202+
return url;
203+
}
204+
205+
// It's a relative URL, use the frame src to create the full URL.
206+
var pathToDir = location.href.substring(0, location.href.lastIndexOf('/'));
207+
208+
return concatenatePaths(pathToDir, url);
209+
}
210+
})();

src/components/iframe/iframe.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { CoreIframeUtilsProvider } from '@providers/utils/iframe';
2525
import { CoreUtilsProvider } from '@providers/utils/utils';
2626
import { CoreSplitViewComponent } from '@components/split-view/split-view';
2727
import { CoreUrl } from '@singletons/url';
28+
import { WKWebViewCookiesWindow } from 'cordova-plugin-wkwebview-cookies';
2829

2930
@Component({
3031
selector: 'core-iframe',
@@ -108,7 +109,7 @@ export class CoreIframeComponent implements OnChanges {
108109
if (this.platform.is('ios') && !this.urlUtils.isLocalFileUrl(url)) {
109110
// Save a "fake" cookie for the iframe's domain to fix a bug in WKWebView.
110111
try {
111-
const win = <any> window;
112+
const win = <WKWebViewCookiesWindow> window;
112113
const urlParts = CoreUrl.parse(url);
113114

114115
await win.WKWebViewCookies.setCookie({

src/core/h5p/assets/moodle/js/embed.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ H5PEmbedCommunicator = (function() {
8080
*/
8181
self.post = function(component, statements) {
8282
window.parent.postMessage({
83-
context: 'moodleapp',
83+
environment: 'moodleapp',
84+
context: 'h5p',
8485
action: 'xapi_post_statement',
8586
component: component,
8687
statements: statements,

src/providers/file.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1229,7 +1229,7 @@ export class CoreFileProvider {
12291229
}
12301230

12311231
/**
1232-
* Get the full path to the www folder at runtime.
1232+
* Get the path to the www folder at runtime based on the WebView URL.
12331233
*
12341234
* @return Path.
12351235
*/
@@ -1243,6 +1243,20 @@ export class CoreFileProvider {
12431243
return window.location.href;
12441244
}
12451245

1246+
/**
1247+
* Get the full path to the www folder.
1248+
*
1249+
* @return Path.
1250+
*/
1251+
getWWWAbsolutePath(): string {
1252+
if (cordova && cordova.file && cordova.file.applicationDirectory) {
1253+
return this.textUtils.concatenatePaths(cordova.file.applicationDirectory, 'www');
1254+
}
1255+
1256+
// Cannot use Cordova to get it, use the WebView URL.
1257+
return this.getWWWPath();
1258+
}
1259+
12461260
/**
12471261
* Helper function to call Ionic WebView convertFileSrc only in the needed platforms.
12481262
* This is needed to make files work with the Ionic WebView plugin.

0 commit comments

Comments
 (0)