// (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 { Directive, Input, OnInit, ElementRef, Optional, SecurityContext } from '@angular/core'; import { SafeUrl } from '@angular/platform-browser'; import { IonContent } from '@ionic/angular'; import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreTextUtils } from '@services/utils/text'; import { CoreConstants } from '@/core/constants'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; import { CoreCustomURLSchemes } from '@services/urlschemes'; import { DomSanitizer } from '@singletons'; import { CoreFilepool } from '@services/filepool'; /** * Directive to open a link in external browser or in the app. */ @Directive({ selector: '[core-link]', }) export class CoreLinkDirective implements OnInit { @Input() href?: string | SafeUrl; // Link URL. @Input() capture?: boolean | string; // If the link needs to be captured by the app. @Input() inApp?: boolean | string; // True to open in embedded browser, false to open in system browser. /* Whether the link should be opened with auto-login. Accepts the following values: "yes" -> Always auto-login. "no" -> Never auto-login. "check" -> Auto-login only if it points to the current site. Default value. */ @Input() autoLogin = 'check'; @Input() showBrowserWarning = true; // Whether to show a warning before opening browser. Defaults to true. protected element: Element; constructor( element: ElementRef, @Optional() protected content: IonContent, ) { this.element = element.nativeElement; } /** * Function executed when the component is initialized. */ ngOnInit(): void { this.inApp = this.inApp === undefined ? this.inApp : CoreUtils.isTrueOrOne(this.inApp); if (this.element.tagName != 'BUTTON' && this.element.tagName != 'A') { this.element.setAttribute('tabindex', '0'); this.element.setAttribute('role', 'button'); } this.element.addEventListener('click', async (event) => { this.performAction(event); }); this.element.addEventListener('keydown', (event: KeyboardEvent) => { if ((event.key == ' ' || event.key == 'Enter')) { event.preventDefault(); event.stopPropagation(); } }); this.element.addEventListener('keyup', (event: KeyboardEvent) => { if ((event.key == ' ' || event.key == 'Enter')) { this.performAction(event); } }); } /** * Perform "click" action. * * @param event Event. * @returns Resolved when done. */ protected async performAction(event: Event): Promise { if (event.defaultPrevented) { return; // Link already treated, stop. } let href: string | null = null; if (this.href) { // Convert the URL back to string if needed. href = typeof this.href === 'string' ? this.href : DomSanitizer.sanitize(SecurityContext.URL, this.href); } href = href || this.element.getAttribute('href') || this.element.getAttribute('xlink:href'); if (!href || CoreUrlUtils.getUrlScheme(href) == 'javascript') { return; } event.preventDefault(); event.stopPropagation(); const openIn = this.element.getAttribute('data-open-in'); if (CoreUtils.isTrueOrOne(this.capture)) { href = CoreTextUtils.decodeURI(href); const treated = await CoreContentLinksHelper.handleLink(href, undefined, true, true); if (!treated) { this.navigate(href, openIn); } } else { this.navigate(href, openIn); } } /** * Convenience function to correctly navigate, open file or url in the browser. * * @param href HREF to be opened. * @param openIn Open In App value coming from data-open-in attribute. * @return Promise resolved when done. */ protected async navigate(href: string, openIn?: string | null): Promise { if (CoreUrlUtils.isLocalFileUrl(href)) { return this.openLocalFile(href); } if (href.charAt(0) == '#') { // Look for id or name. href = href.substring(1); CoreDomUtils.scrollToElementBySelector( this.element.closest('ion-content'), this.content, `#${href}, [name='${href}']`, ); return; } if (CoreCustomURLSchemes.isCustomURL(href)) { try { await CoreCustomURLSchemes.handleCustomURL(href); } catch (error) { CoreCustomURLSchemes.treatHandleCustomURLError(error); } return; } return this.openExternalLink(href, openIn); } /** * Open a local file. * * @param path Path to the file. * @return Promise resolved when done. */ protected async openLocalFile(path: string): Promise { const filename = path.substring(path.lastIndexOf('/') + 1); if (!CoreFileHelper.isOpenableInApp({ filename })) { try { await CoreFileHelper.showConfirmOpenUnsupportedFile(); } catch (error) { return; // Cancelled, stop. } } try { await CoreUtils.openFile(path); } catch (error) { CoreDomUtils.showErrorModal(error); } } /** * Open an external link in the app or in browser. * * @param href HREF to be opened. * @param openIn Open In App value coming from data-open-in attribute. * @return Promise resolved when done. */ protected async openExternalLink(href: string, openIn?: string | null): Promise { // It's an external link, we will open with browser. Check if we need to auto-login. if (!CoreSites.isLoggedIn()) { // Not logged in, cannot auto-login. if (this.inApp) { CoreUtils.openInApp(href); } else { CoreUtils.openInBrowser(href, { showBrowserWarning: this.showBrowserWarning }); } return; } const currentSite = CoreSites.getRequiredCurrentSite(); // Check if URL does not have any protocol, so it's a relative URL. if (!CoreUrlUtils.isAbsoluteURL(href)) { // Add the site URL at the begining. if (href.charAt(0) == '/') { href = currentSite.getURL() + href; } else { href = currentSite.getURL() + '/' + href; } } if (currentSite.isSitePluginFileUrl(href)) { // It's a site file. Check if it's being downloaded right now. const isDownloading = await CoreFilepool.isFileDownloadingByUrl(currentSite.getId(), href); if (isDownloading) { // Wait for the download to finish before opening the file to prevent downloading it twice. const modal = await CoreDomUtils.showModalLoading(); try { const path = await CoreFilepool.downloadUrl(currentSite.getId(), href); return this.openLocalFile(path); } catch { // Error downloading, just open the original URL. } finally { modal.dismiss(); } } } if (this.autoLogin == 'yes') { if (this.inApp) { await currentSite.openInAppWithAutoLogin(href); } else { await currentSite.openInBrowserWithAutoLogin(href, undefined, { showBrowserWarning: this.showBrowserWarning }); } } else if (this.autoLogin == 'no') { if (this.inApp) { CoreUtils.openInApp(href); } else { CoreUtils.openInBrowser(href, { showBrowserWarning: this.showBrowserWarning }); } } else { // Priority order is: core-link inApp attribute > forceOpenLinksIn setting > data-open-in HTML attribute. let openInApp = this.inApp; if (this.inApp === undefined) { if (CoreConstants.CONFIG.forceOpenLinksIn == 'browser') { openInApp = false; } else if (CoreConstants.CONFIG.forceOpenLinksIn == 'app' || openIn == 'app') { openInApp = true; } } if (openInApp) { await currentSite.openInAppWithAutoLoginIfSameSite(href); } else { await currentSite.openInBrowserWithAutoLoginIfSameSite( href, undefined, { showBrowserWarning: this.showBrowserWarning }, ); } } } }