diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 434d6925b..d743d1269 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -27,6 +27,7 @@ import { CoreLoginHelperProvider } from '@core/login/providers/helper'; import { Keyboard } from '@ionic-native/keyboard'; import { ScreenOrientation } from '@ionic-native/screen-orientation'; import { CoreLoginSitesPage } from '@core/login/pages/sites/sites'; +import { CoreWindow } from '@singletons/window'; @Component({ templateUrl: 'app.html' @@ -207,6 +208,11 @@ export class MoodleMobileApp implements OnInit { }); }; + // "Expose" CoreWindow.open. + ( window).openWindowSafely = (url: string, name?: string, windowFeatures?: string): void => { + CoreWindow.open(url, name); + }; + // Load custom lang strings. This cannot be done inside the lang provider because it causes circular dependencies. const loadCustomStrings = (): void => { const currentSite = this.sitesProvider.getCurrentSite(), diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts index 85d063b14..e62e8ba70 100644 --- a/src/core/compile/providers/compile.ts +++ b/src/core/compile/providers/compile.ts @@ -57,7 +57,9 @@ import { Md5 } from 'ts-md5/dist/md5'; // Import core classes that can be useful for site plugins. import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreArray } from '@singletons/array'; import { CoreUrl } from '@singletons/url'; +import { CoreWindow } from '@singletons/window'; import { CoreCache } from '@classes/cache'; import { CoreDelegate } from '@classes/delegate'; import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; @@ -270,7 +272,9 @@ export class CoreCompileProvider { instance['moment'] = moment; instance['Md5'] = Md5; instance['CoreSyncBaseProvider'] = CoreSyncBaseProvider; + instance['CoreArray'] = CoreArray; instance['CoreUrl'] = CoreUrl; + instance['CoreWindow'] = CoreWindow; instance['CoreCache'] = CoreCache; instance['CoreDelegate'] = CoreDelegate; instance['CoreContentLinksHandlerBase'] = CoreContentLinksHandlerBase; diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index 996fab7bf..b5dcf2906 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -446,6 +446,8 @@ export class CoreFormatTextDirective implements OnChanges { } }).then((formatted) => { + formatted = this.treatWindowOpen(formatted); + const div = document.createElement('div'), canTreatVimeo = site && site.isVersionGreaterEqualThan(['3.3.4', '3.4']), navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; @@ -739,4 +741,26 @@ export class CoreFormatTextDirective implements OnChanges { this.iframeUtils.treatFrame(iframe, false, navCtrl); } + + /** + * Convert window.open to window.openWindowSafely inside HTML tags. + * + * @param text Text to treat. + * @return Treated text. + */ + protected treatWindowOpen(text: string): string { + // Get HTML tags that include window.open. Script tags aren't executed so there's no need to treat them. + const matches = text.match(/<[^>]+window\.open\([^\)]*\)[^>]*>/g); + + if (matches) { + matches.forEach((match) => { + // Replace all the window.open inside the tag. + const treated = match.replace(/window\.open\(/g, 'window.openWindowSafely('); + + text = text.replace(match, treated); + }); + } + + return text; + } } diff --git a/src/directives/link.ts b/src/directives/link.ts index 2a18a11e5..bea6e6910 100644 --- a/src/directives/link.ts +++ b/src/directives/link.ts @@ -71,7 +71,7 @@ export class CoreLinkDirective implements OnInit { // If the event prevented default action, do nothing. if (!event.defaultPrevented) { let href = this.element.getAttribute('href'); - if (href) { + if (href && this.urlUtils.getUrlScheme(href) != 'javascript') { event.preventDefault(); event.stopPropagation(); diff --git a/src/providers/utils/iframe.ts b/src/providers/utils/iframe.ts index be188bbb1..29aad5859 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 { CoreWindow } from '@singletons/window'; import { WKUserScriptWindow, WKUserScriptInjectionTime } from 'cordova-plugin-wkuserscript'; /* @@ -387,17 +388,9 @@ export class CoreIframeUtilsProvider { } } 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); - } - } + await CoreWindow.open(url, name, { + navCtrl, + }); } } diff --git a/src/singletons/window.ts b/src/singletons/window.ts new file mode 100644 index 000000000..e95e6dc6e --- /dev/null +++ b/src/singletons/window.ts @@ -0,0 +1,67 @@ +// (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. + +import { NavController } from 'ionic-angular'; +import { CoreSites } from '@providers/sites'; +import { CoreUrlUtils } from '@providers/utils/url'; +import { CoreUtils } from '@providers/utils/utils'; +import { CoreContentLinksHelper } from '@core/contentlinks/providers/helper'; + +/** + * Options for the open function. + */ +export type CoreWindowOpenOptions = { + /** + * NavController to use when opening the link in the app. + */ + navCtrl?: NavController; +}; + +/** + * Singleton with helper functions for windows. + */ +export class CoreWindow { + + /** + * "Safe" implementation of window.open. It will open the URL without overriding the app. + * + * @param url URL to open. + * @param name Name of the browsing context into which to load the URL. + * @param options Other options. + * @return Promise resolved when done. + */ + static async open(url: string, name?: string, options?: CoreWindowOpenOptions): Promise { + if (CoreUrlUtils.instance.isLocalFileUrl(url)) { + await CoreUtils.instance.openFile(url); + } else { + let treated: boolean; + options = options || {}; + + if (name != '_system') { + // Check if it can be opened in the app. + treated = await CoreContentLinksHelper.instance.handleLink(url, undefined, options.navCtrl, true, true); + } + + if (!treated) { + // Not opened in the app, open with browser. Check if we need to auto-login + if (!CoreSites.instance.isLoggedIn()) { + // Not logged in, cannot auto-login. + CoreUtils.instance.openInBrowser(url); + } else { + await CoreSites.instance.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url); + } + } + } + } +}