diff --git a/package-lock.json b/package-lock.json index 14f3ca105..7623fa790 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3768,8 +3768,12 @@ "resolved": "https://registry.npmjs.org/cordova-plugin-whitelist/-/cordova-plugin-whitelist-1.3.4.tgz", "integrity": "sha512-EYC5eQFVkoYXq39l7tYKE6lEjHJ04mvTmKXxGL7quHLdFPfJMNzru/UYpn92AOfpl3PQaZmou78C7EgmFOwFQQ==" }, + "cordova-plugin-wkuserscript": { + "version": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git#6413f4bb3c2565f353e690b5c1450b69ad9e860e", + "from": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git" + }, "cordova-plugin-wkwebview-cookies": { - "version": "git+https://github.com/moodlemobile/cordova-plugin-wkwebview-cookies.git#8e319b9cc5887611bd8972152e4377757986d570", + "version": "git+https://github.com/moodlemobile/cordova-plugin-wkwebview-cookies.git#8c3a289e29b33edecff15f470c1630baf4ec3e88", "from": "git+https://github.com/moodlemobile/cordova-plugin-wkwebview-cookies.git" }, "cordova-plugin-zip": { @@ -6232,8 +6236,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": false, - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "optional": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "aproba": { "version": "1.2.0", @@ -6254,14 +6257,12 @@ "balanced-match": { "version": "1.0.0", "resolved": false, - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "optional": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "brace-expansion": { "version": "1.1.11", "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6276,20 +6277,17 @@ "code-point-at": { "version": "1.1.0", "resolved": false, - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "optional": true + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "concat-map": { "version": "0.0.1", "resolved": false, - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "optional": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "console-control-strings": { "version": "1.1.0", "resolved": false, - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "optional": true + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "core-util-is": { "version": "1.0.2", @@ -6414,8 +6412,7 @@ "inherits": { "version": "2.0.3", "resolved": false, - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "optional": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", @@ -6427,7 +6424,6 @@ "version": "1.0.0", "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6442,7 +6438,6 @@ "version": "3.0.4", "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6486,7 +6481,6 @@ "version": "0.5.1", "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "optional": true, "requires": { "minimist": "0.0.8" }, @@ -6494,8 +6488,7 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "optional": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" } } }, @@ -6594,8 +6587,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": false, - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "optional": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "object-assign": { "version": "4.1.1", @@ -6607,7 +6599,6 @@ "version": "1.4.0", "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "optional": true, "requires": { "wrappy": "1" } @@ -6729,7 +6720,6 @@ "version": "1.0.2", "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6749,7 +6739,6 @@ "version": "3.0.1", "resolved": false, "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6805,8 +6794,7 @@ "wrappy": { "version": "1.0.2", "resolved": false, - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "optional": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "yallist": { "version": "3.0.3", @@ -7290,8 +7278,7 @@ "version": "2.1.1", "resolved": false, "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -7315,15 +7302,13 @@ "version": "1.0.0", "resolved": false, "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7340,22 +7325,19 @@ "version": "1.1.0", "resolved": false, "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": false, "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "resolved": false, "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -7486,8 +7468,7 @@ "version": "2.0.3", "resolved": false, "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -7501,7 +7482,6 @@ "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7518,7 +7498,6 @@ "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7527,15 +7506,13 @@ "version": "0.0.8", "resolved": false, "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "resolved": false, "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -7556,7 +7533,6 @@ "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -7645,8 +7621,7 @@ "version": "1.0.1", "resolved": false, "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -7660,7 +7635,6 @@ "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -7756,8 +7730,7 @@ "version": "5.1.2", "resolved": false, "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -7799,7 +7772,6 @@ "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7821,7 +7793,6 @@ "resolved": false, "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -7870,15 +7841,13 @@ "version": "1.0.2", "resolved": false, "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "resolved": false, "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "dev": true, - "optional": true + "dev": true } } }, @@ -14940,8 +14909,7 @@ "version": "2.1.1", "resolved": false, "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -14965,15 +14933,13 @@ "version": "1.0.0", "resolved": false, "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -14990,22 +14956,19 @@ "version": "1.1.0", "resolved": false, "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": false, "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "resolved": false, "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -15136,8 +15099,7 @@ "version": "2.0.3", "resolved": false, "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -15151,7 +15113,6 @@ "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -15168,7 +15129,6 @@ "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -15177,15 +15137,13 @@ "version": "0.0.8", "resolved": false, "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "resolved": false, "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -15206,7 +15164,6 @@ "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -15295,8 +15252,7 @@ "version": "1.0.1", "resolved": false, "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -15310,7 +15266,6 @@ "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -15406,8 +15361,7 @@ "version": "5.1.2", "resolved": false, "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -15449,7 +15403,6 @@ "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -15471,7 +15424,6 @@ "resolved": false, "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -15520,15 +15472,13 @@ "version": "1.0.2", "resolved": false, "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "resolved": false, "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "dev": true, - "optional": true + "dev": true } } }, diff --git a/package.json b/package.json index 42cc4ff59..8268eef27 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "cordova-plugin-splashscreen": "5.0.3", "cordova-plugin-statusbar": "2.4.3", "cordova-plugin-whitelist": "1.3.4", + "cordova-plugin-wkuserscript": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git", "cordova-plugin-wkwebview-cookies": "git+https://github.com/moodlemobile/cordova-plugin-wkwebview-cookies.git", "cordova-plugin-zip": "3.1.0", "cordova-sqlite-storage": "4.0.0", @@ -211,7 +212,8 @@ }, "cordova-plugin-wkwebview-cookies": {}, "cordova-plugin-qrscanner": {}, - "cordova-plugin-chooser": {} + "cordova-plugin-chooser": {}, + "cordova-plugin-wkuserscript": {} } }, "main": "desktop/electron.js", diff --git a/src/addon/mod/h5pactivity/components/index/index.ts b/src/addon/mod/h5pactivity/components/index/index.ts index 0244d338f..2dc01a2d5 100644 --- a/src/addon/mod/h5pactivity/components/index/index.ts +++ b/src/addon/mod/h5pactivity/components/index/index.ts @@ -407,7 +407,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv * @return Whether it's an XAPI post statement of the current activity. */ protected isCurrentXAPIPost(data: any): boolean { - if (data.context != 'moodleapp' || data.action != 'xapi_post_statement' || !data.statements) { + if (data.environment != 'moodleapp' || data.context != 'h5p' || data.action != 'xapi_post_statement' || !data.statements) { return false; } diff --git a/src/assets/js/iframe-recaptcha.js b/src/assets/js/iframe-recaptcha.js new file mode 100644 index 000000000..f1ff5e843 --- /dev/null +++ b/src/assets/js/iframe-recaptcha.js @@ -0,0 +1,42 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +(function () { + var url = location.href; + + if (!url.match(/^https?:\/\//i) || !url.match(/\/webservice\/recaptcha\.php/i)) { + // Not the recaptcha script, stop. + return; + } + + // Define recaptcha callbacks. + window.recaptchacallback = function(value) { + window.parent.postMessage({ + environment: 'moodleapp', + context: 'recaptcha', + action: 'callback', + frameUrl: location.href, + value: value, + }, '*'); + }; + + window.recaptchaexpiredcallback = function() { + window.parent.postMessage({ + environment: 'moodleapp', + context: 'recaptcha', + action: 'expired', + frameUrl: location.href, + }, '*'); + }; +})(); \ No newline at end of file diff --git a/src/assets/js/iframe-treat-links.js b/src/assets/js/iframe-treat-links.js new file mode 100644 index 000000000..68f83571b --- /dev/null +++ b/src/assets/js/iframe-treat-links.js @@ -0,0 +1,210 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +(function () { + var url = location.href; + + if (url.match(/^moodleappfs:\/\/localhost/i) || !url.match(/^[a-z0-9]+:\/\//i)) { + // Same domain as the app, stop. + return; + } + + // Redefine window.open. + window.open = function(url, name, specs) { + if (name == '_self') { + // Link should be loaded in the same frame. + location.href = toAbsolute(url); + + return; + } + + getRootWindow(window).postMessage({ + environment: 'moodleapp', + context: 'iframe', + action: 'window_open', + frameUrl: location.href, + url: url, + name: name, + specs: specs, + }, '*'); + }; + + // Handle link clicks. + document.addEventListener('click', (event) => { + if (event.defaultPrevented) { + // Event already prevented by some other code. + return; + } + + // Find the link being clicked. + var el = event.target; + while (el && el.tagName !== 'A') { + el = el.parentElement; + } + + if (!el || el.treated) { + return; + } + + // Add click listener to the link, this way if the iframe has added a listener to the link it will be executed first. + el.treated = true; + el.addEventListener('click', function(event) { + linkClicked(el, event); + }); + }, { + capture: true // Use capture to fix this listener not called if the element clicked is too deep in the DOM. + }); + + + + /** + * Concatenate two paths, adding a slash between them if needed. + * + * @param leftPath Left path. + * @param rightPath Right path. + * @return Concatenated path. + */ + function concatenatePaths(leftPath, rightPath) { + if (!leftPath) { + return rightPath; + } else if (!rightPath) { + return leftPath; + } + + var lastCharLeft = leftPath.slice(-1); + var firstCharRight = rightPath.charAt(0); + + if (lastCharLeft === '/' && firstCharRight === '/') { + return leftPath + rightPath.substr(1); + } else if (lastCharLeft !== '/' && firstCharRight !== '/') { + return leftPath + '/' + rightPath; + } else { + return leftPath + rightPath; + } + } + + /** + * Get the root window. + * + * @param win Current window to check. + * @return Root window. + */ + function getRootWindow(win) { + if (win.parent === win) { + return win; + } + + return getRootWindow(win.parent); + } + + /** + * Get the scheme from a URL. + * + * @param url URL to treat. + * @return Scheme, undefined if no scheme found. + */ + function getUrlScheme(url) { + if (!url) { + return; + } + + var matches = url.match(/^([a-z][a-z0-9+\-.]*):/); + if (matches && matches[1]) { + return matches[1]; + } + } + + /** + * Check if a URL is absolute. + * + * @param url URL to treat. + * @return Whether it's absolute. + */ + function isAbsoluteUrl(url) { + return /^[^:]{2,}:\/\//i.test(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. + */ + function isLocalFileUrlScheme(scheme) { + if (scheme) { + scheme = scheme.toLowerCase(); + } + + return scheme == 'cdvfile' || + scheme == 'file' || + scheme == 'filesystem' || + scheme == 'moodleappfs'; + } + + /** + * Handle a click on an anchor element. + * + * @param link Anchor element clicked. + * @param event Click event. + */ + function linkClicked(link, event) { + if (event.defaultPrevented) { + // Event already prevented by some other code. + return; + } + + var linkScheme = getUrlScheme(link.href); + var pageScheme = getUrlScheme(location.href); + var isTargetSelf = !link.target || link.target == '_self'; + + if (!link.href || linkScheme == 'javascript') { + // Links with no URL and Javascript links are ignored. + return; + } + + event.preventDefault(); + + if (isTargetSelf && (isLocalFileUrlScheme(linkScheme) || !isLocalFileUrlScheme(pageScheme))) { + // Link should be loaded in the same frame. Don't do it if link is online and frame is local. + location.href = toAbsolute(link.href); + + return; + } + + getRootWindow(window).postMessage({ + environment: 'moodleapp', + context: 'iframe', + action: 'link_clicked', + frameUrl: location.href, + link: {href: link.href, target: link.target}, + }, '*'); + } + + /** + * Convert a URL to an absolute URL if needed using the frame src. + * + * @param url URL to convert. + * @return Absolute URL. + */ + function toAbsolute(url) { + if (isAbsoluteUrl(url)) { + return url; + } + + // It's a relative URL, use the frame src to create the full URL. + var pathToDir = location.href.substring(0, location.href.lastIndexOf('/')); + + return concatenatePaths(pathToDir, url); + } +})(); \ No newline at end of file diff --git a/src/components/iframe/core-iframe.html b/src/components/iframe/core-iframe.html index 9e5f98ae6..0ba267b63 100644 --- a/src/components/iframe/core-iframe.html +++ b/src/components/iframe/core-iframe.html @@ -1,5 +1,7 @@ -
- +
+ + + diff --git a/src/components/iframe/iframe.ts b/src/components/iframe/iframe.ts index 84560094c..5806412bb 100644 --- a/src/components/iframe/iframe.ts +++ b/src/components/iframe/iframe.ts @@ -13,7 +13,7 @@ // limitations under the License. import { - Component, Input, Output, OnInit, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange, Optional + Component, Input, Output, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange, Optional } from '@angular/core'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { NavController, Platform } from 'ionic-angular'; @@ -25,12 +25,13 @@ import { CoreIframeUtilsProvider } from '@providers/utils/iframe'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreUrl } from '@singletons/url'; +import { WKWebViewCookiesWindow } from 'cordova-plugin-wkwebview-cookies'; @Component({ selector: 'core-iframe', templateUrl: 'core-iframe.html' }) -export class CoreIframeComponent implements OnInit, OnChanges { +export class CoreIframeComponent implements OnChanges { @ViewChild('iframe') iframe: ElementRef; @Input() src: string; @@ -43,6 +44,7 @@ export class CoreIframeComponent implements OnInit, OnChanges { protected logger; protected IFRAME_TIMEOUT = 15000; + protected initialized = false; constructor(logger: CoreLoggerProvider, protected iframeUtils: CoreIframeUtilsProvider, @@ -59,9 +61,15 @@ export class CoreIframeComponent implements OnInit, OnChanges { } /** - * Component being initialized. + * Init the data. */ - ngOnInit(): void { + protected init(): void { + if (this.initialized) { + return; + } + + this.initialized = true; + const iframe: HTMLIFrameElement = this.iframe && this.iframe.nativeElement; this.iframeWidth = this.domUtils.formatPixelsSize(this.iframeWidth) || '100%'; @@ -101,7 +109,7 @@ export class CoreIframeComponent implements OnInit, OnChanges { if (this.platform.is('ios') && !this.urlUtils.isLocalFileUrl(url)) { // Save a "fake" cookie for the iframe's domain to fix a bug in WKWebView. try { - const win = window; + const win = window; const urlParts = CoreUrl.parse(url); await win.WKWebViewCookies.setCookie({ @@ -116,6 +124,11 @@ export class CoreIframeComponent implements OnInit, OnChanges { } this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(CoreFile.instance.convertFileSrc(url)); + + // Now that the URL has been set, initialize the iframe. Wait for the iframe to the added to the DOM. + setTimeout(() => { + this.init(); + }); } } } diff --git a/src/components/recaptcha/recaptchamodal.ts b/src/components/recaptcha/recaptchamodal.ts index 1280e569f..2cff04c18 100644 --- a/src/components/recaptcha/recaptchamodal.ts +++ b/src/components/recaptcha/recaptchamodal.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component } from '@angular/core'; +import { Component, OnDestroy } from '@angular/core'; import { ViewController, NavParams } from 'ionic-angular'; /** @@ -22,13 +22,19 @@ import { ViewController, NavParams } from 'ionic-angular'; selector: 'core-recaptcha-modal', templateUrl: 'core-recaptchamodal.html' }) -export class CoreRecaptchaModalComponent { +export class CoreRecaptchaModalComponent implements OnDestroy { expired = false; value = ''; src: string; + protected messageListenerFunction: (event: MessageEvent) => Promise; + constructor(protected viewCtrl: ViewController, params: NavParams) { this.src = params.get('src'); + + // Listen for messages from the iframe. + this.messageListenerFunction = this.onIframeMessage.bind(this); + window.addEventListener('message', this.messageListenerFunction); } /** @@ -51,18 +57,63 @@ export class CoreRecaptchaModalComponent { const contentWindow = iframe && iframe.contentWindow; if (contentWindow) { - // Set the callbacks we're interested in. - contentWindow['recaptchacallback'] = (value): void => { - this.expired = false; - this.value = value; - this.closeModal(); - }; - - contentWindow['recaptchaexpiredcallback'] = (): void => { - // Verification expired. Check the checkbox again. - this.expired = true; - this.value = ''; - }; + try { + // Set the callbacks we're interested in. + contentWindow['recaptchacallback'] = this.onRecaptchaCallback.bind(this); + contentWindow['recaptchaexpiredcallback'] = this.onRecaptchaExpiredCallback.bind(this); + } catch (error) { + // Cannot access the window. + } } } + + /** + * Treat an iframe message event. + * + * @param event Event. + * @return Promise resolved when done. + */ + protected async onIframeMessage(event: MessageEvent): Promise { + if (!event.data || event.data.environment != 'moodleapp' || event.data.context != 'recaptcha') { + return; + } + + switch (event.data.action) { + case 'callback': + this.onRecaptchaCallback(event.data.value); + break; + case 'expired': + this.onRecaptchaExpiredCallback(); + break; + + default: + break; + } + } + + /** + * Recapcha callback called. + * + * @param value Value received. + */ + protected onRecaptchaCallback(value: any): void { + this.expired = false; + this.value = value; + this.closeModal(); + } + + /** + * Recapcha expired callback called. + */ + protected onRecaptchaExpiredCallback(): void { + this.expired = true; + this.value = ''; + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + window.removeEventListener('message', this.messageListenerFunction); + } } diff --git a/src/core/h5p/assets/moodle/js/embed.js b/src/core/h5p/assets/moodle/js/embed.js index 8df9b26a3..904eded89 100644 --- a/src/core/h5p/assets/moodle/js/embed.js +++ b/src/core/h5p/assets/moodle/js/embed.js @@ -80,7 +80,8 @@ H5PEmbedCommunicator = (function() { */ self.post = function(component, statements) { window.parent.postMessage({ - context: 'moodleapp', + environment: 'moodleapp', + context: 'h5p', action: 'xapi_post_statement', component: component, statements: statements, diff --git a/src/providers/file.ts b/src/providers/file.ts index 684d7f214..173ff4311 100644 --- a/src/providers/file.ts +++ b/src/providers/file.ts @@ -1229,7 +1229,7 @@ export class CoreFileProvider { } /** - * Get the full path to the www folder at runtime. + * Get the path to the www folder at runtime based on the WebView URL. * * @return Path. */ @@ -1243,6 +1243,20 @@ export class CoreFileProvider { return window.location.href; } + /** + * Get the full path to the www folder. + * + * @return Path. + */ + getWWWAbsolutePath(): string { + if (cordova && cordova.file && cordova.file.applicationDirectory) { + return this.textUtils.concatenatePaths(cordova.file.applicationDirectory, 'www'); + } + + // Cannot use Cordova to get it, use the WebView URL. + return this.getWWWPath(); + } + /** * Helper function to call Ionic WebView convertFileSrc only in the needed platforms. * This is needed to make files work with the Ionic WebView plugin. diff --git a/src/providers/utils/iframe.ts b/src/providers/utils/iframe.ts index 551606ead..be188bbb1 100644 --- a/src/providers/utils/iframe.ts +++ b/src/providers/utils/iframe.ts @@ -27,6 +27,7 @@ import { CoreUtilsProvider } from './utils'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { makeSingleton } from '@singletons/core.singletons'; import { CoreUrl } from '@singletons/url'; +import { WKUserScriptWindow, WKUserScriptInjectionTime } from 'cordova-plugin-wkuserscript'; /* * "Utils" service with helper functions for iframes, embed and similar. @@ -43,6 +44,27 @@ export class CoreIframeUtilsProvider { private translate: TranslateService, private network: Network, private zone: NgZone, private config: Config, private contentLinksHelper: CoreContentLinksHelperProvider) { this.logger = logger.getInstance('CoreUtilsProvider'); + + const win = window; + + if (platform.is('ios') && win.WKUserScript) { + platform.ready().then(() => { + // Inject code to the iframes because we cannot access the online ones. + const wwwPath = fileProvider.getWWWAbsolutePath(); + const linksPath = textUtils.concatenatePaths(wwwPath, 'assets/js/iframe-treat-links.js'); + const recaptchaPath = textUtils.concatenatePaths(wwwPath, 'assets/js/iframe-recaptcha.js'); + + win.WKUserScript.addScript({id: 'CoreIframeUtilsLinksScript', file: linksPath}); + win.WKUserScript.addScript({ + id: 'CoreIframeUtilsRecaptchaScript', + file: recaptchaPath, + injectionTime: WKUserScriptInjectionTime.END, + }); + + // Handle post messages received by iframes. + window.addEventListener('message', this.handleIframeMessage.bind(this)); + }); + } } /** @@ -186,6 +208,30 @@ export class CoreIframeUtilsProvider { return { window: contentWindow, document: contentDocument }; } + /** + * Handle some iframe messages. + * + * @param event Message event. + */ + handleIframeMessage(event: MessageEvent): void { + if (!event.data || event.data.environment != 'moodleapp' || event.data.context != 'iframe') { + return; + } + + switch (event.data.action) { + case 'window_open': + this.windowOpen(event.data.url, event.data.name); + break; + + case 'link_clicked': + this.linkClicked(event.data.link); + break; + + default: + break; + } + } + /** * Redefine the open method in the contentWindow of an element and the sub frames. * Please notice that the element should be an iframe, embed or similar. @@ -198,55 +244,9 @@ export class CoreIframeUtilsProvider { redefineWindowOpen(element: any, contentWindow: Window, contentDocument: Document, navCtrl?: NavController): void { if (contentWindow) { // Intercept window.open. - contentWindow.open = (url: string, target: string): Window => { - const scheme = this.urlUtils.getUrlScheme(url); - if (!scheme) { - // It's a relative URL, use the frame src to create the full URL. - const src = element.src || element.data; - if (src) { - const dirAndFile = this.fileProvider.getFileAndDirectoryFromPath(src); - if (dirAndFile.directory) { - url = this.textUtils.concatenatePaths(dirAndFile.directory, url); - } else { - this.logger.warn('Cannot get iframe dir path to open relative url', url, element); + contentWindow.open = (url: string, name: string): Window => { + this.windowOpen(url, name, element, navCtrl); - return null; - } - } else { - this.logger.warn('Cannot get iframe src to open relative url', url, element); - - return null; - } - } - - if (target == '_self') { - // Link should be loaded in the same frame. - if (element.tagName.toLowerCase() == 'object') { - element.setAttribute('data', url); - } else { - element.setAttribute('src', url); - } - } else if (this.urlUtils.isLocalFileUrl(url)) { - // It's a local file. - this.utils.openFile(url).catch((error) => { - this.domUtils.showErrorModal(error); - }); - } else { - // It's an external link, check if it can be opened in the app. - this.contentLinksHelper.handleLink(url, undefined, navCtrl, true, true).then((treated) => { - if (!treated) { - // Not opened in the app, open with browser. Check if we need to auto-login - if (!this.sitesProvider.isLoggedIn()) { - // Not logged in, cannot auto-login. - this.utils.openInBrowser(url); - } else { - this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url); - } - } - }); - } - - // We cannot create new Window objects directly, return null which is a valid return value for Window.open(). return null; }; } @@ -329,21 +329,88 @@ export class CoreIframeUtilsProvider { // Add click listener to the link, this way if the iframe has added a listener to the link it will be executed first. link.treated = true; - link.addEventListener('click', this.linkClicked.bind(this, element, link)); + link.addEventListener('click', this.linkClicked.bind(this, link, element)); }, { capture: true // Use capture to fix this listener not called if the element clicked is too deep in the DOM. }); } + /** + * Handle a window.open called by a frame. + * + * @param url URL passed to window.open. + * @param name Name passed to window.open. + * @param element HTML element of the frame. + * @param navCtrl NavController to use if a link can be opened in the app. + * @return Promise resolved when done. + */ + protected async windowOpen(url: string, name: string, element?: any, navCtrl?: NavController): Promise { + const scheme = this.urlUtils.getUrlScheme(url); + if (!scheme) { + // It's a relative URL, use the frame src to create the full URL. + const src = element && (element.src || element.data); + if (src) { + const dirAndFile = this.fileProvider.getFileAndDirectoryFromPath(src); + if (dirAndFile.directory) { + url = this.textUtils.concatenatePaths(dirAndFile.directory, url); + } else { + this.logger.warn('Cannot get iframe dir path to open relative url', url, element); + + return; + } + } else { + this.logger.warn('Cannot get iframe src to open relative url', url, element); + + return; + } + } + + if (name == '_self') { + // Link should be loaded in the same frame. + if (!element) { + this.logger.warn('Cannot load URL in iframe because the element was not supplied', url); + + return; + } + + if (element.tagName.toLowerCase() == 'object') { + element.setAttribute('data', url); + } else { + element.setAttribute('src', url); + } + } else if (this.urlUtils.isLocalFileUrl(url)) { + // It's a local file. + try { + await this.utils.openFile(url); + } catch (error) { + this.domUtils.showErrorModal(error); + } + } else { + // It's an external link, check if it can be opened in the app. + const treated = await this.contentLinksHelper.handleLink(url, undefined, navCtrl, true, true); + + if (!treated) { + // Not opened in the app, open with browser. Check if we need to auto-login + if (!this.sitesProvider.isLoggedIn()) { + // Not logged in, cannot auto-login. + this.utils.openInBrowser(url); + } else { + await this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url); + } + } + } + } + /** * A link inside a frame was clicked. * + * @param link Data of the link clicked. * @param element Frame element. - * @param link Link clicked. * @param event Click event. */ - protected linkClicked(element: HTMLFrameElement | HTMLObjectElement, link: HTMLAnchorElement, event: Event): void { - if (event.defaultPrevented) { + protected linkClicked(link: {href: string, target?: string}, element?: HTMLFrameElement | HTMLObjectElement, event?: Event) + : void { + if (event && event.defaultPrevented) { // Event already prevented by some other code. return; } @@ -356,12 +423,12 @@ export class CoreIframeUtilsProvider { if (!this.urlUtils.isLocalFileUrlScheme(urlParts.protocol, urlParts.domain)) { // Scheme suggests it's an external resource. - event.preventDefault(); + event && event.preventDefault(); - const frameSrc = ( element).src || ( element).data; + const frameSrc = element && (( element).src || ( element).data); // If the frame is not local, check the target to identify how to treat the link. - if (!this.urlUtils.isLocalFileUrl(frameSrc) && (!link.target || link.target == '_self')) { + if (element && !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); @@ -380,13 +447,13 @@ export class CoreIframeUtilsProvider { } } else if (link.target == '_parent' || link.target == '_top' || link.target == '_blank') { // Opening links with _parent, _top or _blank can break the app. We'll open it in InAppBrowser. - event.preventDefault(); + event && event.preventDefault(); this.utils.openFile(link.href).catch((error) => { this.domUtils.showErrorModal(error); }); - } else if (this.platform.is('ios') && (!link.target || link.target == '_self')) { + } else if (this.platform.is('ios') && (!link.target || link.target == '_self') && element) { // In cordova ios 4.1.0 links inside iframes stopped working. We'll manually treat them. - event.preventDefault(); + event && event.preventDefault(); if (element.tagName.toLowerCase() == 'object') { element.setAttribute('data', link.href); } else {