From e72fea3630f36f7aba9915f2708e33d6cc958db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 15 Nov 2024 13:06:55 +0100 Subject: [PATCH] MOBILE-4653 utils: Move opener related utils functions --- .../airnotifier/services/airnotifier.ts | 4 +- .../bigbluebuttonbn/components/index/index.ts | 3 +- src/addons/mod/lti/services/lti.ts | 7 +- .../mod/resource/components/index/index.ts | 4 +- .../mod/resource/services/resource-helper.ts | 4 +- .../mod/workshop/components/index/index.ts | 3 +- .../components/phase-modal/phase-modal.ts | 4 +- .../services/handlers/push-click.ts | 6 +- src/app/app.component.ts | 4 +- src/core/classes/sites/site.ts | 11 +- src/core/components/file/file.ts | 8 +- src/core/components/local-file/local-file.ts | 6 +- src/core/components/recaptcha/recaptcha.ts | 4 +- src/core/directives/link.ts | 13 +- .../directives/download-module-main-file.ts | 4 +- .../features/course/services/course-helper.ts | 21 +- .../site-onboarding/site-onboarding.ts | 4 +- .../pages/change-password/change-password.ts | 8 +- .../login/pages/email-signup/email-signup.ts | 3 +- .../features/login/services/login-helper.ts | 16 +- .../policy/pages/site-policy/site-policy.ts | 3 +- .../components/report-detail/report-detail.ts | 4 +- .../complete-profile/complete-profile.ts | 4 +- .../user/services/handlers/profile-mail.ts | 4 +- src/core/features/user/services/support.ts | 4 +- .../initializers/prepare-inapp-browser.ts | 13 +- src/core/services/file-helper.ts | 10 +- src/core/services/filepool.ts | 9 +- src/core/services/sites.ts | 3 +- src/core/services/utils/dom.ts | 6 +- src/core/services/utils/iframe.ts | 14 +- src/core/services/utils/mimetype.ts | 26 +- src/core/services/utils/utils.ts | 212 +-------- src/core/singletons/iab.ts | 182 -------- src/core/singletons/opener.ts | 433 ++++++++++++++++++ src/core/singletons/window.ts | 44 +- 36 files changed, 598 insertions(+), 510 deletions(-) delete mode 100644 src/core/singletons/iab.ts create mode 100644 src/core/singletons/opener.ts diff --git a/src/addons/messageoutput/airnotifier/services/airnotifier.ts b/src/addons/messageoutput/airnotifier/services/airnotifier.ts index 6b6220d4b..022c20a52 100644 --- a/src/addons/messageoutput/airnotifier/services/airnotifier.ts +++ b/src/addons/messageoutput/airnotifier/services/airnotifier.ts @@ -22,7 +22,7 @@ import { CoreWSError } from '@classes/errors/wserror'; import { makeSingleton, Translate } from '@singletons'; import { CoreEvents, CoreEventSiteData } from '@singletons/events'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreOpener } from '@singletons/opener'; import { CorePath } from '@singletons/path'; import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site'; @@ -222,7 +222,7 @@ export class AddonMessageOutputAirnotifierProvider { ); // Don't try auto-login, admins cannot use it. - CoreUtils.openInBrowser(url, { + CoreOpener.openInBrowser(url, { showBrowserWarning: false, }); }, diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.ts b/src/addons/mod/bigbluebuttonbn/components/index/index.ts index 461617dd4..36498b7dc 100644 --- a/src/addons/mod/bigbluebuttonbn/components/index/index.ts +++ b/src/addons/mod/bigbluebuttonbn/components/index/index.ts @@ -35,6 +35,7 @@ import { ADDON_MOD_BBB_COMPONENT } from '../../constants'; import { CoreLoadings } from '@services/loadings'; import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; import { CorePromiseUtils } from '@singletons/promise-utils'; +import { CoreOpener } from '@singletons/opener'; /** * Component that displays a Big Blue Button activity. @@ -303,7 +304,7 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo try { const joinUrl = await AddonModBBB.getJoinUrl(this.module.id, this.groupId); - await CoreUtils.openInBrowser(joinUrl, { + await CoreOpener.openInBrowser(joinUrl, { showBrowserWarning: false, }); diff --git a/src/addons/mod/lti/services/lti.ts b/src/addons/mod/lti/services/lti.ts index 0a4f7bb52..84d8b62fd 100644 --- a/src/addons/mod/lti/services/lti.ts +++ b/src/addons/mod/lti/services/lti.ts @@ -23,12 +23,11 @@ import { CorePlatform } from '@services/platform'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; import { CoreText } from '@singletons/text'; import { CoreUrl } from '@singletons/url'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreOpener } from '@singletons/opener'; import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { ADDON_MOD_LTI_COMPONENT } from '../constants'; import { CoreCacheUpdateFrequency } from '@/core/constants'; -import { CoreInAppBrowser } from '@singletons/iab'; /** * Service that provides some features for LTI. @@ -254,10 +253,10 @@ export class AddonModLtiProvider { const launcherUrl = await this.generateLauncher(url, params); if (CorePlatform.isMobile()) { - CoreInAppBrowser.open(launcherUrl); + CoreOpener.openInApp(launcherUrl); } else { // In desktop open in browser, we found some cases where inapp caused JS issues. - CoreUtils.openInBrowser(launcherUrl); + CoreOpener.openInBrowser(launcherUrl); } } diff --git a/src/addons/mod/resource/components/index/index.ts b/src/addons/mod/resource/components/index/index.ts index 1af37bd5a..58a961e56 100644 --- a/src/addons/mod/resource/components/index/index.ts +++ b/src/addons/mod/resource/components/index/index.ts @@ -25,7 +25,6 @@ import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreText } from '@singletons/text'; -import { CoreUtils, OpenFileAction } from '@services/utils/utils'; import { NgZone, Translate } from '@singletons'; import { Subscription } from 'rxjs'; import { @@ -36,6 +35,7 @@ import { AddonModResourceHelper } from '../../services/resource-helper'; import { CorePlatform } from '@services/platform'; import { ADDON_MOD_RESOURCE_COMPONENT } from '../../constants'; import { CorePromiseUtils } from '@singletons/promise-utils'; +import { OpenFileAction } from '@singletons/opener'; /** * Component that displays a resource. @@ -176,7 +176,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource this.readableSize = CoreText.bytesToSize(this.module.contentsinfo.filessize, 1); this.timemodified = this.module.contentsinfo.lastmodified * 1000; } else { - mimetype = await CoreUtils.getMimeTypeFromUrl(CoreFileHelper.getFileUrl(contents[0])); + mimetype = await CoreMimetypeUtils.getMimeTypeFromUrl(CoreFileHelper.getFileUrl(contents[0])); this.readableSize = CoreText.bytesToSize(contents[0].filesize, 1); this.timemodified = contents[0].timemodified * 1000; } diff --git a/src/addons/mod/resource/services/resource-helper.ts b/src/addons/mod/resource/services/resource-helper.ts index 0f9fc20b6..84ce659f7 100644 --- a/src/addons/mod/resource/services/resource-helper.ts +++ b/src/addons/mod/resource/services/resource-helper.ts @@ -24,7 +24,6 @@ import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; -import { CoreUtilsOpenFileOptions } from '@services/utils/utils'; import { makeSingleton, Translate } from '@singletons'; import { CorePath } from '@singletons/path'; import { AddonModResource, AddonModResourceCustomData } from './resource'; @@ -33,6 +32,7 @@ import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { ADDON_MOD_RESOURCE_COMPONENT } from '../constants'; import { CoreLoadings } from '@services/loadings'; +import { CoreOpenerOpenFileOptions } from '@singletons/opener'; /** * Service that provides helper functions for resources. @@ -190,7 +190,7 @@ export class AddonModResourceHelperProvider { * @param options Options to open the file. * @returns Resolved when done. */ - async openModuleFile(module: CoreCourseModuleData, courseId: number, options: CoreUtilsOpenFileOptions = {}): Promise { + async openModuleFile(module: CoreCourseModuleData, courseId: number, options: CoreOpenerOpenFileOptions = {}): Promise { const modal = await CoreLoadings.show(); try { diff --git a/src/addons/mod/workshop/components/index/index.ts b/src/addons/mod/workshop/components/index/index.ts index 173989eb7..38cb33567 100644 --- a/src/addons/mod/workshop/components/index/index.ts +++ b/src/addons/mod/workshop/components/index/index.ts @@ -58,6 +58,7 @@ import { AddonModWorkshopPhase, } from '@addons/mod/workshop/constants'; import { CorePromiseUtils } from '@singletons/promise-utils'; +import { CoreOpener } from '@singletons/opener'; /** * Component that displays a workshop index page. @@ -363,7 +364,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity if (task.code == 'submit') { this.gotoSubmit(); } else if (task.link) { - CoreUtils.openInBrowser(task.link); + CoreOpener.openInBrowser(task.link); } } diff --git a/src/addons/mod/workshop/components/phase-modal/phase-modal.ts b/src/addons/mod/workshop/components/phase-modal/phase-modal.ts index a6a138526..1091cb673 100644 --- a/src/addons/mod/workshop/components/phase-modal/phase-modal.ts +++ b/src/addons/mod/workshop/components/phase-modal/phase-modal.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, Input, OnInit } from '@angular/core'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreOpener } from '@singletons/opener'; import { ModalController } from '@singletons'; import { AddonModWorkshopPhaseData, AddonModWorkshopPhaseTaskData } from '../../services/workshop'; import { AddonModWorkshopPhase } from '../../constants'; @@ -69,7 +69,7 @@ export class AddonModWorkshopPhaseInfoModalComponent implements OnInit { // This will close the modal and go to the submit. ModalController.dismiss(true); } else if (task.link) { - CoreUtils.openInBrowser(task.link); + CoreOpener.openInBrowser(task.link); } } diff --git a/src/addons/notifications/services/handlers/push-click.ts b/src/addons/notifications/services/handlers/push-click.ts index 42a890f5d..77b1593cd 100644 --- a/src/addons/notifications/services/handlers/push-click.ts +++ b/src/addons/notifications/services/handlers/push-click.ts @@ -24,8 +24,8 @@ import { AddonNotifications } from '../notifications'; import { AddonNotificationsMainMenuHandlerService } from './mainmenu'; import { AddonNotificationsHelper } from '../notifications-helper'; import { CoreViewer } from '@features/viewer/services/viewer'; -import { CoreInAppBrowser } from '@singletons/iab'; import { CorePromiseUtils } from '@singletons/promise-utils'; +import { CoreOpener } from '@singletons/opener'; /** * Handler for non-messaging push notifications clicks. @@ -91,12 +91,12 @@ export class AddonNotificationsPushClickHandlerService implements CorePushNotifi switch (notification.customdata.appurlopenin) { case 'inapp': - CoreInAppBrowser.open(url); + CoreOpener.openInApp(url); return; case 'browser': - return CoreUtils.openInBrowser(url); + return CoreOpener.openInBrowser(url); default: { const treated = await CoreContentLinksHelper.handleLink(url, undefined, undefined, true); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index f8907f867..f3d264144 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -22,12 +22,12 @@ import { CoreApp } from '@services/app'; import { CoreNavigator } from '@services/navigator'; import { CoreSubscriptions } from '@singletons/subscriptions'; import { CoreWindow } from '@singletons/window'; -import { CoreInAppBrowser } from '@singletons/iab'; import { CorePlatform } from '@services/platform'; import { CoreLogger } from '@singletons/logger'; import { CorePromisedValue } from '@classes/promised-value'; import { register } from 'swiper/element/bundle'; import { CoreWait } from '@singletons/wait'; +import { CoreOpener } from '@singletons/opener'; register(); @@ -51,7 +51,7 @@ export class AppComponent implements OnInit, AfterViewInit { CorePlatform.resume.subscribe(() => { // Wait a second before setting it to false since in iOS there could be some frozen WS calls. setTimeout(() => { - if (CoreLoginHelper.isWaitingForBrowser() && !CoreInAppBrowser.isInAppBrowserOpen()) { + if (CoreLoginHelper.isWaitingForBrowser() && !CoreOpener.isInAppBrowserOpen()) { CoreLoginHelper.stopWaitingForBrowser(); CoreLoginHelper.checkLogout(); } diff --git a/src/core/classes/sites/site.ts b/src/core/classes/sites/site.ts index c8ae21dd9..2d386468f 100644 --- a/src/core/classes/sites/site.ts +++ b/src/core/classes/sites/site.ts @@ -27,7 +27,7 @@ import { import { CoreDomUtils } from '@services/utils/dom'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUrl } from '@singletons/url'; -import { CoreUtils, CoreUtilsOpenInBrowserOptions } from '@services/utils/utils'; +import { CoreOpener, CoreOpenerOpenInBrowserOptions } from '@singletons/opener'; import { CoreConstants } from '@/core/constants'; import { SQLiteDB } from '@classes/sqlitedb'; import { CoreError } from '@classes/errors/error'; @@ -55,7 +55,6 @@ import { CoreAuthenticatedSite, CoreAuthenticatedSiteOptionalData, CoreSiteWSPre import { firstValueFrom } from 'rxjs'; import { CorePlatform } from '@services/platform'; import { CoreLoadings } from '@services/loadings'; -import { CoreInAppBrowser } from '@singletons/iab'; import { CorePromiseUtils } from '@singletons/promise-utils'; /** @@ -472,7 +471,7 @@ export class CoreSite extends CoreAuthenticatedSite { async openInBrowserWithAutoLogin( url: string, alertMessage?: string, - options: CoreUtilsOpenInBrowserOptions = {}, + options: CoreOpenerOpenInBrowserOptions = {}, ): Promise { await this.openWithAutoLogin(false, url, options, alertMessage); } @@ -503,7 +502,7 @@ export class CoreSite extends CoreAuthenticatedSite { async openWithAutoLogin( inApp: boolean, url: string, - options: InAppBrowserOptions & CoreUtilsOpenInBrowserOptions = {}, + options: InAppBrowserOptions & CoreOpenerOpenInBrowserOptions = {}, alertMessage?: string, ): Promise { // Get the URL to open. @@ -537,9 +536,9 @@ export class CoreSite extends CoreAuthenticatedSite { options.clearsessioncache = 'yes'; } - return CoreInAppBrowser.open(autoLoginUrl, options); + return CoreOpener.openInApp(autoLoginUrl, options); } else { - return CoreUtils.openInBrowser(autoLoginUrl, options); + return CoreOpener.openInBrowser(autoLoginUrl, options); } } diff --git a/src/core/components/file/file.ts b/src/core/components/file/file.ts index fedc1f106..260fe3c88 100644 --- a/src/core/components/file/file.ts +++ b/src/core/components/file/file.ts @@ -21,7 +21,6 @@ import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreUrl } from '@singletons/url'; -import { CoreUtils, CoreUtilsOpenFileOptions, OpenFileAction } from '@services/utils/utils'; import { CoreText } from '@singletons/text'; import { DownloadStatus } from '@/core/constants'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -29,6 +28,7 @@ import { CoreWSFile } from '@services/ws'; import { CorePlatform } from '@services/platform'; import { toBoolean } from '@/core/transforms/boolean'; import { CorePromiseUtils } from '@singletons/promise-utils'; +import { CoreOpener, CoreOpenerOpenFileOptions, OpenFileAction } from '@singletons/opener'; /** * Component to handle a remote file. Shows the file name, icon (depending on mimetype) and a button @@ -155,7 +155,7 @@ export class CoreFileComponent implements OnInit, OnDestroy { return; } - const options: CoreUtilsOpenFileOptions = {}; + const options: CoreOpenerOpenFileOptions = {}; if (isOpenButton) { // Use the non-default method. options.iOSOpenFileAction = this.defaultIsOpenWithPicker ? OpenFileAction.OPEN : OpenFileAction.OPEN_WITH; @@ -194,9 +194,9 @@ export class CoreFileComponent implements OnInit, OnDestroy { if (!this.canDownload || !this.state || this.state === DownloadStatus.NOT_DOWNLOADABLE) { // File cannot be downloaded, just open it. if (CoreUrl.isLocalFileUrl(this.fileUrl)) { - CoreUtils.openFile(this.fileUrl); + CoreOpener.openFile(this.fileUrl); } else { - CoreUtils.openOnlineFile(CoreUrl.unfixPluginfileURL(this.fileUrl)); + CoreOpener.openOnlineFile(CoreUrl.unfixPluginfileURL(this.fileUrl)); } return; diff --git a/src/core/components/local-file/local-file.ts b/src/core/components/local-file/local-file.ts index f1f5fdcad..e05d3865e 100644 --- a/src/core/components/local-file/local-file.ts +++ b/src/core/components/local-file/local-file.ts @@ -23,7 +23,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; -import { CoreUtils, CoreUtilsOpenFileOptions, OpenFileAction } from '@services/utils/utils'; +import { CoreOpener, CoreOpenerOpenFileOptions, OpenFileAction } from '@singletons/opener'; import { CoreForms } from '@singletons/form'; import { CorePath } from '@singletons/path'; import { CorePlatform } from '@services/platform'; @@ -134,13 +134,13 @@ export class CoreLocalFileComponent implements OnInit { } } - const options: CoreUtilsOpenFileOptions = {}; + const options: CoreOpenerOpenFileOptions = {}; if (isOpenButton) { // Use the non-default method. options.iOSOpenFileAction = this.defaultIsOpenWithPicker ? OpenFileAction.OPEN : OpenFileAction.OPEN_WITH; } - CoreUtils.openFile(CoreFile.getFileEntryURL(this.file), options); + CoreOpener.openFile(CoreFile.getFileEntryURL(this.file), options); } /** diff --git a/src/core/components/recaptcha/recaptcha.ts b/src/core/components/recaptcha/recaptcha.ts index beb129e37..23428bb23 100644 --- a/src/core/components/recaptcha/recaptcha.ts +++ b/src/core/components/recaptcha/recaptcha.ts @@ -17,7 +17,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { CoreLang, CoreLangFormat } from '@services/lang'; import { CoreSites } from '@services/sites'; -import { CoreInAppBrowser } from '@singletons/iab'; +import { CoreOpener } from '@singletons/opener'; import { CorePath } from '@singletons/path'; /** @@ -66,7 +66,7 @@ export class CoreRecaptchaComponent implements OnInit { // The app cannot render the recaptcha directly because it has problems with the local protocols and domains. const src = CorePath.concatenatePaths(this.siteUrl, 'webservice/recaptcha.php?lang=' + this.lang); - const inAppBrowserWindow = CoreInAppBrowser.open(src); + const inAppBrowserWindow = CoreOpener.openInApp(src); if (!inAppBrowserWindow) { return; } diff --git a/src/core/directives/link.ts b/src/core/directives/link.ts index 8ba268e87..0f38c8375 100644 --- a/src/core/directives/link.ts +++ b/src/core/directives/link.ts @@ -19,7 +19,7 @@ import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUrl } from '@singletons/url'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreOpener } from '@singletons/opener'; import { CoreConstants } from '@/core/constants'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; import { CoreCustomURLSchemes } from '@services/urlschemes'; @@ -28,7 +28,6 @@ import { CoreFilepool } from '@services/filepool'; import { CoreDom } from '@singletons/dom'; import { toBoolean } from '../transforms/boolean'; import { CoreLoadings } from '@services/loadings'; -import { CoreInAppBrowser } from '@singletons/iab'; /** * Directive to open a link in external browser or in the app. @@ -164,7 +163,7 @@ export class CoreLinkDirective implements OnInit { } try { - await CoreUtils.openFile(path); + await CoreOpener.openFile(path); } catch (error) { CoreDomUtils.showErrorModal(error); } @@ -187,9 +186,9 @@ export class CoreLinkDirective implements OnInit { if (!CoreSites.isLoggedIn()) { // Not logged in, cannot auto-login. if (openInApp) { - CoreInAppBrowser.open(href); + CoreOpener.openInApp(href); } else { - CoreUtils.openInBrowser(href, { showBrowserWarning: this.showBrowserWarning }); + CoreOpener.openInBrowser(href, { showBrowserWarning: this.showBrowserWarning }); } return; @@ -228,9 +227,9 @@ export class CoreLinkDirective implements OnInit { } } else { if (openInApp) { - CoreInAppBrowser.open(href); + CoreOpener.openInApp(href); } else { - CoreUtils.openInBrowser(href, { showBrowserWarning: this.showBrowserWarning }); + CoreOpener.openInBrowser(href, { showBrowserWarning: this.showBrowserWarning }); } } } diff --git a/src/core/features/course/directives/download-module-main-file.ts b/src/core/features/course/directives/download-module-main-file.ts index 603be6748..82f95acab 100644 --- a/src/core/features/course/directives/download-module-main-file.ts +++ b/src/core/features/course/directives/download-module-main-file.ts @@ -17,8 +17,8 @@ import { Directive, Input, OnInit, ElementRef } from '@angular/core'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreCourse, CoreCourseModuleContentFile } from '@features/course/services/course'; import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper'; -import { CoreUtilsOpenFileOptions } from '@services/utils/utils'; import { CoreLoadings } from '@services/loadings'; +import { CoreOpenerOpenFileOptions } from '@singletons/opener'; /** * Directive to allow downloading and open the main file of a module. @@ -38,7 +38,7 @@ export class CoreCourseDownloadModuleMainFileDirective implements OnInit { @Input() component?: string; // Component to link the file to. @Input() componentId?: string | number; // Component ID to use in conjunction with the component. If not defined, use moduleId. @Input() files?: CoreCourseModuleContentFile[]; // List of files of the module. If not provided, use module.contents. - @Input() options?: CoreUtilsOpenFileOptions = {}; + @Input() options?: CoreOpenerOpenFileOptions = {}; protected element: HTMLElement; diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 189669b15..e4c7f7c4d 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -32,7 +32,7 @@ import { CoreLogger } from '@singletons/logger'; import { ApplicationInit, makeSingleton, Translate } from '@singletons'; import { CoreFilepool } from '@services/filepool'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUtils, CoreUtilsOpenFileOptions } from '@services/utils/utils'; +import { CoreUtils } from '@services/utils/utils'; import { CoreCourseAnyCourseData, CoreCourseBasicData, @@ -83,6 +83,7 @@ import { CORE_COURSE_COMPONENT, } from '../constants'; import { CorePromiseUtils } from '@singletons/promise-utils'; +import { CoreOpener, CoreOpenerOpenFileOptions } from '@singletons/opener'; /** * Prefetch info of a module. @@ -640,7 +641,7 @@ export class CoreCourseHelperProvider { componentId?: string | number, files?: CoreCourseModuleContentFile[], siteId?: string, - options: CoreUtilsOpenFileOptions = {}, + options: CoreOpenerOpenFileOptions = {}, ): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); @@ -678,7 +679,7 @@ export class CoreCourseHelperProvider { ); if (CoreUrl.isLocalFileUrl(result.path)) { - return CoreUtils.openFile(result.path, options); + return CoreOpener.openFile(result.path, options); } /* In iOS, if we use the same URL in embedded browser and background download then the download only @@ -686,7 +687,7 @@ export class CoreCourseHelperProvider { result.path = result.path + '#moodlemobile-embedded'; try { - await CoreUtils.openOnlineFile(result.path); + await CoreOpener.openOnlineFile(result.path); } catch (error) { // Error opening the file, some apps don't allow opening online files. if (!CoreFile.isAvailable()) { @@ -706,7 +707,7 @@ export class CoreCourseHelperProvider { path = await CoreFilepool.getInternalUrlByUrl(siteId, mainFile.fileurl); } - await CoreUtils.openFile(path, options); + await CoreOpener.openFile(path, options); } } @@ -731,7 +732,7 @@ export class CoreCourseHelperProvider { component?: string, componentId?: string | number, files?: CoreCourseModuleContentFile[], - options: CoreUtilsOpenFileOptions = {}, + options: CoreOpenerOpenFileOptions = {}, ): Promise { if (!CoreNetwork.isOnline()) { // Not online, get the offline file. It will fail if not found. @@ -742,7 +743,7 @@ export class CoreCourseHelperProvider { throw new CoreNetworkError(); } - return CoreUtils.openFile(path, options); + return CoreOpener.openFile(path, options); } // Open in browser. @@ -754,7 +755,7 @@ export class CoreCourseHelperProvider { // Remove forcedownload when not followed by any param. fixedUrl = fixedUrl.replace(/[?|&]forcedownload=\d+/, ''); - CoreUtils.openInBrowser(fixedUrl); + CoreOpener.openInBrowser(fixedUrl); if (CoreFile.isAvailable()) { // Download the file if needed (file outdated or not downloaded). @@ -783,7 +784,7 @@ export class CoreCourseHelperProvider { componentId?: string | number, files?: CoreCourseModuleContentFile[], siteId?: string, - options: CoreUtilsOpenFileOptions = {}, + options: CoreOpenerOpenFileOptions = {}, ): Promise<{ fixedUrl: string; path: string; status?: DownloadStatus }> { siteId = siteId || CoreSites.getCurrentSiteId(); @@ -881,7 +882,7 @@ export class CoreCourseHelperProvider { component?: string, componentId?: string | number, siteId?: string, - options: CoreUtilsOpenFileOptions = {}, + options: CoreOpenerOpenFileOptions = {}, ): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); diff --git a/src/core/features/login/components/site-onboarding/site-onboarding.ts b/src/core/features/login/components/site-onboarding/site-onboarding.ts index da8e6b39e..3bf0f10c3 100644 --- a/src/core/features/login/components/site-onboarding/site-onboarding.ts +++ b/src/core/features/login/components/site-onboarding/site-onboarding.ts @@ -15,7 +15,7 @@ import { Component } from '@angular/core'; import { CoreConfig } from '@services/config'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreOpener } from '@singletons/opener'; import { GET_STARTED_URL, ONBOARDING_DONE } from '@features/login/constants'; import { ModalController } from '@singletons'; import { CoreSharedModule } from '@/core/shared.module'; @@ -84,7 +84,7 @@ export class CoreLoginSiteOnboardingComponent { this.saveOnboardingDone(); - CoreUtils.openInBrowser(GET_STARTED_URL, { showBrowserWarning: false }); + CoreOpener.openInBrowser(GET_STARTED_URL, { showBrowserWarning: false }); ModalController.dismiss(); } diff --git a/src/core/features/login/pages/change-password/change-password.ts b/src/core/features/login/pages/change-password/change-password.ts index c033584c3..97e759f1e 100644 --- a/src/core/features/login/pages/change-password/change-password.ts +++ b/src/core/features/login/pages/change-password/change-password.ts @@ -19,8 +19,8 @@ import { CoreLoginHelper } from '@features/login/services/login-helper'; import { Translate } from '@singletons'; import { CoreNavigator } from '@services/navigator'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; -import { CoreInAppBrowser } from '@singletons/iab'; import { CoreUserSupport } from '@features/user/services/support'; +import { CoreOpener } from '@singletons/opener'; /** * Page that shows instructions to change the password. @@ -94,7 +94,7 @@ export class CoreLoginChangePasswordPage implements OnDestroy { this.urlLoadedObserver = CoreEvents.on(CoreEvents.IAB_LOAD_STOP, (event) => { if (event.url.match(/\/login\/change_password\.php.*return=1/)) { // Password has changed, close the IAB now. - CoreInAppBrowser.closeInAppBrowser(); + CoreOpener.closeInAppBrowser(); this.login(); return; @@ -105,7 +105,7 @@ export class CoreLoginChangePasswordPage implements OnDestroy { } // Use a script to check if the user changed the password, in some platforms we cannot tell using the URL. - CoreInAppBrowser.getInAppBrowserInstance()?.executeScript({ + CoreOpener.getInAppBrowserInstance()?.executeScript({ code: ` if ( document.querySelector('input[type="password"]') === null && @@ -119,7 +119,7 @@ export class CoreLoginChangePasswordPage implements OnDestroy { this.messageObserver = CoreEvents.on(CoreEvents.IAB_MESSAGE, (data) => { if (data.passwordChanged) { - CoreInAppBrowser.closeInAppBrowser(); + CoreOpener.closeInAppBrowser(); this.login(); } }); diff --git a/src/core/features/login/pages/email-signup/email-signup.ts b/src/core/features/login/pages/email-signup/email-signup.ts index 7c7e1c5ac..9c3e6129d 100644 --- a/src/core/features/login/pages/email-signup/email-signup.ts +++ b/src/core/features/login/pages/email-signup/email-signup.ts @@ -39,6 +39,7 @@ import { CoreInputErrorsMessages } from '@components/input-errors/input-errors'; import { CoreViewer } from '@features/viewer/services/viewer'; import { CoreLoadings } from '@services/loadings'; import { CorePromiseUtils } from '@singletons/promise-utils'; +import { CoreOpener } from '@singletons/opener'; /** * Page to signup using email. @@ -395,7 +396,7 @@ export class CoreLoginEmailSignupPage implements OnInit { * Show contact information on site (we have to display again the age verification form). */ showContactOnSite(): void { - CoreUtils.openInBrowser( + CoreOpener.openInBrowser( CorePath.concatenatePaths(this.site.getURL(), '/login/verify_age_location.php'), { showBrowserWarning: false }, ); diff --git a/src/core/features/login/services/login-helper.ts b/src/core/features/login/services/login-helper.ts index f25cd3d64..f877011c4 100644 --- a/src/core/features/login/services/login-helper.ts +++ b/src/core/features/login/services/login-helper.ts @@ -62,8 +62,8 @@ import { CoreQRScan } from '@services/qrscan'; import { CoreLoadings } from '@services/loadings'; import { CoreErrorHelper } from '@services/error-helper'; import { CoreSSO } from '@singletons/sso'; -import { CoreInAppBrowser } from '@singletons/iab'; import { CorePromiseUtils } from '@singletons/promise-utils'; +import { CoreOpener } from '@singletons/opener'; /** * Helper provider that provides some common features regarding authentication. @@ -174,7 +174,7 @@ export class CoreLoginHelperProvider { async forgottenPasswordClicked(siteUrl: string, username: string, siteConfig?: CoreSitePublicConfigResponse): Promise { if (siteConfig && siteConfig.forgottenpasswordurl) { // URL set, open it. - CoreInAppBrowser.open(siteConfig.forgottenpasswordurl); + CoreOpener.openInApp(siteConfig.forgottenpasswordurl); return; } @@ -628,7 +628,7 @@ export class CoreLoginHelperProvider { }); // Always open it in browser because the user might have the session stored in there. - CoreUtils.openInBrowser(loginUrl, { showBrowserWarning: false }); + CoreOpener.openInBrowser(loginUrl, { showBrowserWarning: false }); CoreApp.closeApp(); return true; @@ -665,12 +665,12 @@ export class CoreLoginHelperProvider { this.logger.debug('openBrowserForSSOLogin loginUrl:', loginUrl); if (this.isSSOEmbeddedBrowser(typeOfLogin)) { - CoreInAppBrowser.open(loginUrl, { + CoreOpener.openInApp(loginUrl, { clearsessioncache: 'yes', // Clear the session cache to allow for multiple logins. closebuttoncaption: Translate.instant('core.login.cancel'), }); } else { - CoreUtils.openInBrowser(loginUrl, { showBrowserWarning: false }); + CoreOpener.openInBrowser(loginUrl, { showBrowserWarning: false }); CoreApp.closeApp(); } } catch (error) { @@ -692,7 +692,7 @@ export class CoreLoginHelperProvider { await alert.onDidDismiss(); - CoreInAppBrowser.open(siteUrl + '/login/change_password.php'); + CoreOpener.openInApp(siteUrl + '/login/change_password.php'); } /** @@ -701,7 +701,7 @@ export class CoreLoginHelperProvider { * @param siteUrl URL of the site. */ openForgottenPassword(siteUrl: string): void { - CoreInAppBrowser.open(siteUrl + '/login/forgot_password.php'); + CoreOpener.openInApp(siteUrl + '/login/forgot_password.php'); } /** @@ -977,7 +977,7 @@ export class CoreLoginHelperProvider { async openInBrowserFallback(siteUrl: string, debug?: CoreSiteErrorDebug): Promise { CoreEvents.trigger(APP_UNSUPPORTED_CHURN, { siteUrl, debug }); - await CoreUtils.openInBrowser(siteUrl, { showBrowserWarning: false }); + await CoreOpener.openInBrowser(siteUrl, { showBrowserWarning: false }); } /** diff --git a/src/core/features/policy/pages/site-policy/site-policy.ts b/src/core/features/policy/pages/site-policy/site-policy.ts index da486b70d..94be08c61 100644 --- a/src/core/features/policy/pages/site-policy/site-policy.ts +++ b/src/core/features/policy/pages/site-policy/site-policy.ts @@ -16,7 +16,6 @@ import { ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, ViewChild import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUtils } from '@services/utils/utils'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreSite } from '@classes/sites/site'; import { CoreNavigator } from '@services/navigator'; @@ -141,7 +140,7 @@ export class CorePolicySitePolicyPage implements OnInit, OnDestroy { // Try to get the mime type. try { - const mimeType = await CoreUtils.getMimeTypeFromUrl(this.sitePoliciesURL); + const mimeType = await CoreMimetypeUtils.getMimeTypeFromUrl(this.sitePoliciesURL); const extension = CoreMimetypeUtils.getExtension(mimeType, this.sitePoliciesURL); this.showInline = extension == 'html' || extension == 'htm'; diff --git a/src/core/features/reportbuilder/components/report-detail/report-detail.ts b/src/core/features/reportbuilder/components/report-detail/report-detail.ts index c681a2081..8fe12e1e0 100644 --- a/src/core/features/reportbuilder/components/report-detail/report-detail.ts +++ b/src/core/features/reportbuilder/components/report-detail/report-detail.ts @@ -27,7 +27,7 @@ import { CoreScreen } from '@services/screen'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreErrorObject } from '@services/error-helper'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreOpener } from '@singletons/opener'; import { Translate } from '@singletons'; import { CoreTime } from '@singletons/time'; import { BehaviorSubject, Observable } from 'rxjs'; @@ -150,7 +150,7 @@ export class CoreReportBuilderReportDetailComponent implements OnInit { handler: async () => { const site = CoreSites.getRequiredCurrentSite(); const href = `${site.getURL()}/reportbuilder/view.php?id=${this.reportId}`; - await CoreUtils.openInBrowser(href, { showBrowserWarning: false }); + await CoreOpener.openInBrowser(href, { showBrowserWarning: false }); await CoreNavigator.back(); }, }, diff --git a/src/core/features/user/pages/complete-profile/complete-profile.ts b/src/core/features/user/pages/complete-profile/complete-profile.ts index 53046d6af..4e930652e 100644 --- a/src/core/features/user/pages/complete-profile/complete-profile.ts +++ b/src/core/features/user/pages/complete-profile/complete-profile.ts @@ -19,8 +19,8 @@ import { CoreLoginHelper } from '@features/login/services/login-helper'; import { Translate } from '@singletons'; import { CoreNavigator } from '@services/navigator'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; -import { CoreInAppBrowser } from '@singletons/iab'; import { CoreUserSupport } from '@features/user/services/support'; +import { CoreOpener } from '@singletons/opener'; /** * Page that shows instructions to complete the profile. @@ -93,7 +93,7 @@ export class CoreUserCompleteProfilePage implements OnDestroy { this.urlLoadedObserver = CoreEvents.on(CoreEvents.IAB_LOAD_START, (event) => { if (event.url.match(/\/user\/preferences.php/)) { // Profile should be complete now. - CoreInAppBrowser.closeInAppBrowser(); + CoreOpener.closeInAppBrowser(); this.login(); } }); diff --git a/src/core/features/user/services/handlers/profile-mail.ts b/src/core/features/user/services/handlers/profile-mail.ts index 7a23a58ef..4f966f3a7 100644 --- a/src/core/features/user/services/handlers/profile-mail.ts +++ b/src/core/features/user/services/handlers/profile-mail.ts @@ -20,7 +20,7 @@ import { CoreUserProfileHandlerData, } from '../user-delegate'; import { CoreSites } from '@services/sites'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreOpener } from '@singletons/opener'; import { CoreUserProfile } from '../user'; import { makeSingleton } from '@singletons'; @@ -60,7 +60,7 @@ export class CoreUserProfileMailHandlerService implements CoreUserProfileHandler event.preventDefault(); event.stopPropagation(); - CoreUtils.openInBrowser('mailto:' + user.email, { showBrowserWarning: false }); + CoreOpener.openInBrowser('mailto:' + user.email, { showBrowserWarning: false }); }, }; } diff --git a/src/core/features/user/services/support.ts b/src/core/features/user/services/support.ts index f61e5688e..f76c4a2de 100644 --- a/src/core/features/user/services/support.ts +++ b/src/core/features/user/services/support.ts @@ -18,7 +18,6 @@ import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/suppo import { InAppBrowserObject } from '@awesome-cordova-plugins/in-app-browser'; import { CorePlatform } from '@services/platform'; import { CoreSites } from '@services/sites'; -import { CoreInAppBrowser } from '@singletons/iab'; import { makeSingleton, Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { CoreSubscriptions } from '@singletons/subscriptions'; @@ -26,6 +25,7 @@ import { AlertButton } from '@ionic/angular'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreLang } from '@services/lang'; import { CoreUserNullSupportConfig } from '@features/user/classes/support/null-support-config'; +import { CoreOpener } from '@singletons/opener'; /** * Handle site support. @@ -42,7 +42,7 @@ export class CoreUserSupportService { const supportConfig = options.supportConfig ?? CoreUserAuthenticatedSupportConfig.forCurrentSite(); const supportPageUrl = supportConfig.getSupportPageUrl(); const autoLoginUrl = await CoreSites.getCurrentSite()?.getAutoLoginUrl(supportPageUrl, false); - const browser = CoreInAppBrowser.open(autoLoginUrl ?? supportPageUrl); + const browser = CoreOpener.openInApp(autoLoginUrl ?? supportPageUrl); if (supportPageUrl.endsWith('/user/contactsitesupport.php')) { this.populateSupportForm(browser, options.subject, options.message); diff --git a/src/core/initializers/prepare-inapp-browser.ts b/src/core/initializers/prepare-inapp-browser.ts index c22754cce..30ec93d55 100644 --- a/src/core/initializers/prepare-inapp-browser.ts +++ b/src/core/initializers/prepare-inapp-browser.ts @@ -20,10 +20,9 @@ import { CoreSites } from '@services/sites'; import { CoreCustomURLSchemes } from '@services/urlschemes'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUrl } from '@singletons/url'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreOpener } from '@singletons/opener'; import { Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; -import { CoreInAppBrowser } from '@singletons/iab'; let lastInAppUrl: string | null = null; @@ -44,14 +43,14 @@ export default function(): void { CoreCustomURLSchemes.handleCustomURL(url).catch((error) => { CoreCustomURLSchemes.treatHandleCustomURLError(error); }); - CoreInAppBrowser.closeInAppBrowser(); + CoreOpener.closeInAppBrowser(); return; } if (isExternalApp && url.includes('://token=')) { // It's an SSO token for another app. Close the IAB and show an error. - CoreInAppBrowser.closeInAppBrowser(); + CoreOpener.closeInAppBrowser(); CoreDomUtils.showErrorModal(new CoreSiteError({ supportConfig: CoreSites.getCurrentSite() ? CoreUserAuthenticatedSupportConfig.forCurrentSite() @@ -69,15 +68,15 @@ export default function(): void { } // Open in browser should launch the right app if found and do nothing if not found. - CoreUtils.openInBrowser(url, { showBrowserWarning: false }); + CoreOpener.openInBrowser(url, { showBrowserWarning: false }); // At this point, URL schemes will stop working in IAB, and in Android the IAB is showing a "Webpage not available" error. // Re-loading the page inside the existing IAB doesn't fix it, we need to re-load the whole IAB. if (lastInAppUrl) { - CoreInAppBrowser.open(lastInAppUrl); + CoreOpener.openInApp(lastInAppUrl); } else { // No last URL loaded, close the InAppBrowser. - CoreInAppBrowser.closeInAppBrowser(); + CoreOpener.closeInAppBrowser(); } }); diff --git a/src/core/services/file-helper.ts b/src/core/services/file-helper.ts index 7644b6196..9dd331ad7 100644 --- a/src/core/services/file-helper.ts +++ b/src/core/services/file-helper.ts @@ -23,7 +23,7 @@ import { CoreSites } from '@services/sites'; import { CoreWS, CoreWSFile } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUrl } from '@singletons/url'; -import { CoreUtils, CoreUtilsOpenFileOptions, OpenFileAction } from '@services/utils/utils'; +import { CoreOpener, CoreOpenerOpenFileOptions, OpenFileAction } from '@singletons/opener'; import { CoreConstants, DownloadStatus } from '@/core/constants'; import { CoreError } from '@classes/errors/error'; import { makeSingleton, Translate } from '@singletons'; @@ -69,7 +69,7 @@ export class CoreFileHelperProvider { state?: DownloadStatus, onProgress?: CoreFileHelperOnProgress, siteId?: string, - options: CoreUtilsOpenFileOptions = {}, + options: CoreOpenerOpenFileOptions = {}, ): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); @@ -102,7 +102,7 @@ export class CoreFileHelperProvider { url = url + '#moodlemobile-embedded'; try { - await CoreUtils.openOnlineFile(url); + await CoreOpener.openOnlineFile(url); return; } catch (error) { @@ -130,7 +130,7 @@ export class CoreFileHelperProvider { } } - return CoreUtils.openFile(url, options); + return CoreOpener.openFile(url, options); } /** @@ -156,7 +156,7 @@ export class CoreFileHelperProvider { state?: DownloadStatus, onProgress?: CoreFileHelperOnProgress, siteId?: string, - options: CoreUtilsOpenFileOptions = {}, + options: CoreOpenerOpenFileOptions = {}, ): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 5b040f843..a415a5868 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -27,7 +27,7 @@ import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; -import { CoreUtils, CoreUtilsOpenFileOptions } from '@services/utils/utils'; +import { CoreUtils } from '@services/utils/utils'; import { CoreError } from '@classes/errors/error'; import { DownloadStatus } from '@/core/constants'; import { ApplicationInit, makeSingleton, NgZone, Translate } from '@singletons'; @@ -60,6 +60,7 @@ import { CorePromisedValue } from '@classes/promised-value'; import { CoreAnalytics, CoreAnalyticsEventType } from './analytics'; import { convertTextToHTMLElement } from '../utils/create-html-element'; import { CorePromiseUtils } from '@singletons/promise-utils'; +import { CoreOpener, CoreOpenerOpenFileOptions } from '@singletons/opener'; /* * Factory for handling downloading files and retrieve downloaded files. @@ -2949,18 +2950,18 @@ export class CoreFilepoolProvider { * - The file cannot be streamed. * If the file is big and can be streamed, the promise returned by this function will be rejected. */ - async shouldDownloadFileBeforeOpen(url: string, size: number, options: CoreUtilsOpenFileOptions = {}): Promise { + async shouldDownloadFileBeforeOpen(url: string, size: number, options: CoreOpenerOpenFileOptions = {}): Promise { if (size >= 0 && size <= CoreFilepoolProvider.DOWNLOAD_THRESHOLD) { // The file is small, download it. return true; } - if (CoreUtils.shouldOpenWithDialog(options)) { + if (CoreOpener.shouldOpenWithDialog(options)) { // Open with dialog needs a local file. return true; } - const mimetype = await CoreUtils.getMimeTypeFromUrl(url); + const mimetype = await CoreMimetypeUtils.getMimeTypeFromUrl(url); // If the file is streaming (audio or video), return false. return !CoreMimetypeUtils.isStreamedMimetype(mimetype); diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index 5de4d1b70..168574f0c 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -71,6 +71,7 @@ import { CoreQueueRunner } from '@classes/queue-runner'; import { CoreAppDB } from './app-db'; import { CoreRedirects } from '@singletons/redirects'; import { CorePromiseUtils } from '@singletons/promise-utils'; +import { CoreOpener } from '@singletons/opener'; export const CORE_SITE_SCHEMAS = new InjectionToken('CORE_SITE_SCHEMAS'); export const CORE_SITE_CURRENT_SITE_ID_CONFIG = 'current_site_id'; @@ -947,7 +948,7 @@ export class CoreSitesProvider { Translate.instant('core.updaterequired'), Translate.instant('core.download'), Translate.instant(siteId ? 'core.mainmenu.logout' : 'core.cancel'), - ).then(() => CoreUtils.openInBrowser(downloadUrl, { showBrowserWarning: false })).catch(() => { + ).then(() => CoreOpener.openInBrowser(downloadUrl, { showBrowserWarning: false })).catch(() => { // Do nothing. }); } else { diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 165c6595b..d2f11a7fd 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -22,7 +22,7 @@ import { CoreFile } from '@services/file'; import { CoreWSExternalWarning } from '@services/ws'; import { CoreText } from '@singletons/text'; import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreOpener } from '@singletons/opener'; import { CoreConstants } from '@/core/constants'; import { CoreIonLoadingElement } from '@classes/ion-loading'; import { CoreCanceledError } from '@classes/errors/cancelederror'; @@ -1199,7 +1199,7 @@ export class CoreDomUtilsProvider { buttons.push({ text: Translate.instant('core.download'), handler: (): void => { - CoreUtils.openInBrowser(link, { showBrowserWarning: false }); + CoreOpener.openInBrowser(link, { showBrowserWarning: false }); }, }); } @@ -1428,7 +1428,7 @@ export class CoreDomUtilsProvider { event.preventDefault(); event.stopPropagation(); - CoreUtils.openInBrowser(href); + CoreOpener.openInBrowser(href); } }); }); diff --git a/src/core/services/utils/iframe.ts b/src/core/services/utils/iframe.ts index 062c8571c..a6f2ead98 100644 --- a/src/core/services/utils/iframe.ts +++ b/src/core/services/utils/iframe.ts @@ -22,7 +22,7 @@ import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUrl } from '@singletons/url'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreOpener } from '@singletons/opener'; import { makeSingleton, NgZone, Translate } from '@singletons'; import { CoreLogger } from '@singletons/logger'; @@ -520,7 +520,7 @@ export class CoreIframeUtilsProvider { // The frame is local or the link needs to be opened in a new window. Open in browser. if (!CoreSites.isLoggedIn()) { - CoreUtils.openInBrowser(link.href); + CoreOpener.openInBrowser(link.href); } else { await CoreSites.getCurrentSite()?.openInBrowserWithAutoLogin(link.href); } @@ -539,7 +539,7 @@ export class CoreIframeUtilsProvider { } try { - await CoreUtils.openFile(link.href); + await CoreOpener.openFile(link.href); } catch (error) { CoreDomUtils.showErrorModal(error); } @@ -687,7 +687,7 @@ export class CoreIframeUtilsProvider { undefined; if (localUrl) { - CoreUtils.openFile(localUrl); + CoreOpener.openFile(localUrl); } else { CoreDomUtils.showErrorModal('core.networkerrormsg', true); } @@ -695,11 +695,11 @@ export class CoreIframeUtilsProvider { return; } - const mimetype = await CorePromiseUtils.ignoreErrors(CoreUtils.getMimeTypeFromUrl(url)); + const mimetype = await CorePromiseUtils.ignoreErrors(CoreMimetypeUtils.getMimeTypeFromUrl(url)); if (!mimetype || mimetype === 'text/html' || mimetype === 'text/plain') { // It's probably a web page, open in browser. - options.site ? options.site.openInBrowserWithAutoLogin(url) : CoreUtils.openInBrowser(url); + options.site ? options.site.openInBrowserWithAutoLogin(url) : CoreOpener.openInBrowser(url); return; } @@ -711,7 +711,7 @@ export class CoreIframeUtilsProvider { url = await options.site.checkAndFixPluginfileURL(url); } - CoreUtils.openOnlineFile(url); + CoreOpener.openOnlineFile(url); } finally { modal.dismiss(); diff --git a/src/core/services/utils/mimetype.ts b/src/core/services/utils/mimetype.ts index 9b54f8d69..448734af5 100644 --- a/src/core/services/utils/mimetype.ts +++ b/src/core/services/utils/mimetype.ts @@ -20,7 +20,7 @@ import { CoreFileUtils } from '@singletons/file-utils'; import { CoreText } from '@singletons/text'; import { makeSingleton, Translate } from '@singletons'; import { CoreLogger } from '@singletons/logger'; -import { CoreWSFile } from '@services/ws'; +import { CoreWS, CoreWSFile } from '@services/ws'; import extToMime from '@/assets/exttomime.json'; import mimeToExt from '@/assets/mimetoext.json'; @@ -538,6 +538,30 @@ export class CoreMimetypeUtilsProvider { return this.getFileIconForType(icon); } + /** + * Get the mimetype of a file given its URL. It'll try to guess it using the URL, if that fails then it'll + * perform a HEAD request to get it. It's done in this order because pluginfile.php can return wrong mimetypes. + * This function is in here instead of MimetypeUtils to prevent circular dependencies. + * + * @param url The URL of the file. + * @returns Promise resolved with the mimetype. + */ + async getMimeTypeFromUrl(url: string): Promise { + // First check if it can be guessed from the URL. + const extension = CoreMimetypeUtils.guessExtensionFromUrl(url); + const mimetype = extension && CoreMimetypeUtils.getMimeType(extension); + + // Ignore PHP extension for now, it could be serving a file. + if (mimetype && extension !== 'php') { + return mimetype; + } + + // Can't be guessed, get the remote mimetype. + const remoteMimetype = await CoreWS.getRemoteFileMimeType(url); + + return remoteMimetype || mimetype || ''; + } + /** * Given a group name, return the translated name. * diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 8de7d80bf..d82bdac14 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -15,30 +15,21 @@ import { Injectable } from '@angular/core'; import { InAppBrowserObject } from '@awesome-cordova-plugins/in-app-browser'; import { FileEntry } from '@awesome-cordova-plugins/file/ngx'; -import { CoreFile } from '@services/file'; import { CoreFileUtils } from '@singletons/file-utils'; -import { CoreLang, CoreLangFormat } from '@services/lang'; +import { CoreLang } from '@services/lang'; import { CoreWS } from '@services/ws'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; -import { makeSingleton, FileOpener, WebIntent, Translate } from '@singletons'; +import { makeSingleton, Translate } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreFileEntry } from '@services/file-helper'; -import { CoreConstants } from '@/core/constants'; -import { CoreWindow } from '@singletons/window'; -import { CorePlatform } from '@services/platform'; -import { CoreErrorWithOptions } from '@classes/errors/errorwithoptions'; -import { CoreFilepool } from '@services/filepool'; -import { CoreSites } from '@services/sites'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; -import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreUrl } from '@singletons/url'; import { CoreArray } from '@singletons/array'; import { CoreText } from '@singletons/text'; import { CoreWait, CoreWaitOptions } from '@singletons/wait'; import { CoreQRScan } from '@services/qrscan'; import { CoreErrorHelper } from '@services/error-helper'; -import { CoreInAppBrowser, CoreInAppBrowserOpenOptions } from '@singletons/iab'; import { CorePromiseUtils, OrderedPromiseData } from '@singletons/promise-utils'; +import { CoreOpener, CoreOpenerOpenFileOptions, CoreOpenerOpenInBrowserOptions } from '@singletons/opener'; export type TreeNode = T & { children: TreeNode[] }; @@ -248,30 +239,30 @@ export class CoreUtilsProvider { /** * Close the InAppBrowser window. * - * @deprecated since 5.0. Use CoreInAppBrowser.closeInAppBrowser instead. + * @deprecated since 5.0. Use CoreOpener.closeInAppBrowser instead. */ closeInAppBrowser(): void { - CoreInAppBrowser.closeInAppBrowser(); + CoreOpener.closeInAppBrowser(); } /** * Get inapp browser instance (if any). * * @returns IAB instance, undefined if not open. - * @deprecated since 5.0. Use CoreInAppBrowser.getInAppBrowserInstance instead. + * @deprecated since 5.0. Use CoreOpener.getInAppBrowserInstance instead. */ getInAppBrowserInstance(): InAppBrowserObject | undefined { - return CoreInAppBrowser.getInAppBrowserInstance(); + return CoreOpener.getInAppBrowserInstance(); } /** * Check if inapp browser is open. * * @returns Whether it's open. - * @deprecated since 5.0. Use CoreInAppBrowser.isInAppBrowserOpen instead. + * @deprecated since 5.0. Use CoreOpener.isInAppBrowserOpen instead. */ isInAppBrowserOpen(): boolean { - return CoreInAppBrowser.isInAppBrowserOpen(); + return CoreOpener.isInAppBrowserOpen(); } /** @@ -898,129 +889,35 @@ export class CoreUtilsProvider { * * @param path The local path of the file to be open. * @param options Options. - * @returns Promise resolved when done. + * @deprecated since 5.0. Use CoreOpener.openFile instead. */ - async openFile(path: string, options: CoreUtilsOpenFileOptions = {}): Promise { - // Convert the path to a native path if needed. - path = CoreFile.unconvertFileSrc(path); - - const extension = CoreMimetypeUtils.getFileExtension(path); - const mimetype = extension && CoreMimetypeUtils.getMimeType(extension); - - if (mimetype == 'text/html' && CorePlatform.isAndroid()) { - // Open HTML local files in InAppBrowser, in system browser some embedded files aren't loaded. - CoreInAppBrowser.open(path); - - return; - } else if (extension === 'apk' && CorePlatform.isAndroid()) { - const url = await CorePromiseUtils.ignoreErrors( - CoreFilepool.getFileUrlByPath(CoreSites.getCurrentSiteId(), CoreFile.removeBasePath(path)), - ); - - // @todo MOBILE-4167: Handle urls with expired tokens. - - throw new CoreErrorWithOptions( - Translate.instant('core.cannotinstallapkinfo'), - Translate.instant('core.cannotinstallapk'), - url - ? [ - { - text: Translate.instant('core.openinbrowser'), - handler: () => this.openInBrowser(url), - }, - { - text: Translate.instant('core.cancel'), - role: 'cancel', - }, - ] - : undefined, - ); - } - - // Path needs to be decoded, the file won't be opened if the path has %20 instead of spaces and so. - try { - path = decodeURIComponent(path); - } catch { - // Error, use the original path. - } - - const openFile = async (mimetype?: string) => { - if (this.shouldOpenWithDialog(options)) { - await FileOpener.showOpenWithDialog(path, mimetype || ''); - } else { - await FileOpener.open(path, mimetype || ''); - } - }; - - try { - try { - await openFile(mimetype); - } catch (error) { - if (!extension || !error || Number(error.status) !== 9) { - throw error; - } - - // Cannot open mimetype. Check if there is a deprecated mimetype for the extension. - const deprecatedMimetype = CoreMimetypeUtils.getDeprecatedMimeType(extension); - if (!deprecatedMimetype || deprecatedMimetype === mimetype) { - throw error; - } - - await openFile(deprecatedMimetype); - } - } catch (error) { - this.logger.error('Error opening file ' + path + ' with mimetype ' + mimetype); - this.logger.error('Error: ', JSON.stringify(error)); - - if (!extension || extension.indexOf('/') > -1 || extension.indexOf('\\') > -1) { - // Extension not found. - throw new Error(Translate.instant('core.erroropenfilenoextension')); - } - - throw new Error(Translate.instant('core.erroropenfilenoapp')); - } + async openFile(path: string, options: CoreOpenerOpenFileOptions = {}): Promise { + await CoreOpener.openFile(path, options); } /** * Open a URL using InAppBrowser. - * Do not use for files, refer to {@link CoreUtils.openFile}. + * Do not use for files, refer to {@link CoreOpener.openFile}. * * @param url The URL to open. * @param options Override default options passed to InAppBrowser. * @returns The opened window. * - * @deprecated since 5.0. Use CoreInAppBrowser.openInApp instead. + * @deprecated since 5.0. Use CoreOpener.openInApp instead. */ - openInApp(url: string, options?: CoreInAppBrowserOpenOptions): InAppBrowserObject { - return CoreInAppBrowser.open(url, options); + openInApp(url: string, options?: CoreOpenerOpenFileOptions): InAppBrowserObject { + return CoreOpener.openInApp(url, options); } /** * Open a URL using a browser. - * Do not use for files, refer to {@link CoreUtilsProvider.openFile}. * * @param url The URL to open. * @param options Options. + * @deprecated since 5.0. Use CoreOpener.openInBrowser instead. */ - async openInBrowser(url: string, options: CoreUtilsOpenInBrowserOptions = {}): Promise { - // eslint-disable-next-line deprecation/deprecation - const originaUrl = CoreUrl.unfixPluginfileURL(options.originalUrl ?? options.browserWarningUrl ?? url); - if (options.showBrowserWarning || options.showBrowserWarning === undefined) { - try { - await CoreWindow.confirmOpenBrowserIfNeeded(originaUrl); - } catch { - return; // Cancelled, stop. - } - } - - const site = CoreSites.getCurrentSite(); - CoreAnalytics.logEvent({ type: CoreAnalyticsEventType.OPEN_LINK, link: originaUrl }); - window.open( - site?.containsUrl(url) - ? CoreUrl.addParamsToUrl(url, { lang: await CoreLang.getCurrentLanguage(CoreLangFormat.LMS) }) - : url, - '_system', - ); + async openInBrowser(url: string, options: CoreOpenerOpenInBrowserOptions = {}): Promise { + await CoreOpener.openInBrowser(url, options); } /** @@ -1028,43 +925,10 @@ export class CoreUtilsProvider { * Specially useful for audio and video since they can be streamed. * * @param url The URL of the file. - * @returns Promise resolved when opened. + * @deprecated since 5.0. Use CoreOpener.openOnlineFile instead. */ async openOnlineFile(url: string): Promise { - if (CorePlatform.isAndroid()) { - // In Android we need the mimetype to open it. - const mimetype = await CorePromiseUtils.ignoreErrors(this.getMimeTypeFromUrl(url)); - - if (!mimetype) { - // Couldn't retrieve mimetype. Return error. - throw new Error(Translate.instant('core.erroropenfilenoextension')); - } - - const options = { - action: WebIntent.ACTION_VIEW, - url, - type: mimetype, - }; - - try { - await WebIntent.startActivity(options); - - CoreAnalytics.logEvent({ - type: CoreAnalyticsEventType.OPEN_LINK, - link: CoreUrl.unfixPluginfileURL(url), - }); - - return; - } catch (error) { - this.logger.error('Error opening online file ' + url + ' with mimetype ' + mimetype); - this.logger.error('Error: ', JSON.stringify(error)); - - throw new Error(Translate.instant('core.erroropenfilenoapp')); - } - } - - // In the rest of platforms we need to open them in InAppBrowser. - CoreInAppBrowser.open(url); + await CoreOpener.openOnlineFile(url); } /** @@ -1558,11 +1422,10 @@ export class CoreUtilsProvider { * * @param options Options. * @returns Boolean. + * @deprecated since 5.0. Use CoreOpener.shouldOpenWithDialog instead. */ - shouldOpenWithDialog(options: CoreUtilsOpenFileOptions = {}): boolean { - const openFileAction = options.iOSOpenFileAction ?? CoreConstants.CONFIG.iOSDefaultOpenFileAction; - - return CorePlatform.isIOS() && openFileAction == OpenFileAction.OPEN_WITH; + shouldOpenWithDialog(options: CoreOpenerOpenFileOptions = {}): boolean { + return CoreOpener.shouldOpenWithDialog(options); } } @@ -1585,36 +1448,9 @@ export type CoreMenuItem = { value: T | number; }; -/** - * Options for opening a file. - */ -export type CoreUtilsOpenFileOptions = { - iOSOpenFileAction?: OpenFileAction; // Action to do when opening a file. -}; - -/** - * Options for opening in browser. - */ -export type CoreUtilsOpenInBrowserOptions = { - showBrowserWarning?: boolean; // Whether to display a warning before opening in browser. Defaults to true. - originalUrl?: string; // Original URL to open (in case the URL was treated, e.g. to add a token or an auto-login). - /** - * @deprecated since 4.3. Use originalUrl instead. - */ - browserWarningUrl?: string; -}; - /** * Options for waiting. * * @deprecated since 4.5. Use CoreWaitOptions instead. */ export type CoreUtilsWaitOptions = CoreWaitOptions; - -/** - * Possible default picker actions. - */ -export enum OpenFileAction { - OPEN = 'open', - OPEN_WITH = 'open-with', -} diff --git a/src/core/singletons/iab.ts b/src/core/singletons/iab.ts deleted file mode 100644 index 97e5543fb..000000000 --- a/src/core/singletons/iab.ts +++ /dev/null @@ -1,182 +0,0 @@ -// (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 { InAppBrowserObject, InAppBrowserOptions } from '@awesome-cordova-plugins/in-app-browser'; -import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CorePlatform } from '@services/platform'; -import { InAppBrowser, NgZone } from '@singletons'; -import { CoreEvents } from '@singletons/events'; -import { CoreUrl } from './url'; -import { CoreConstants } from '../constants'; -import { CoreColors } from './colors'; - -/** - * Singleton with helper functions for InAppBrowser. - */ -export class CoreInAppBrowser { - - private static iabInstance?: InAppBrowserObject; - - /** - * Close the InAppBrowser window. - */ - static closeInAppBrowser(): void { - if (!CoreInAppBrowser.iabInstance) { - return; - } - - CoreInAppBrowser.iabInstance.close(); - } - - /** - * Get inapp browser instance (if any). - * - * @returns IAB instance, undefined if not open. - */ - static getInAppBrowserInstance(): InAppBrowserObject | undefined { - return CoreInAppBrowser.iabInstance; - } - - /** - * Check if inapp browser is open. - * - * @returns Whether it's open. - */ - static isInAppBrowserOpen(): boolean { - return !!CoreInAppBrowser.iabInstance; - } - - /** - * Open a URL using InAppBrowser. - * Do not use for files, refer to CoreUtils.openFile. - * - * @param url The URL to open. - * @param options Override default options passed to InAppBrowser. - * @returns The opened window. - */ - static open(url: string, options?: CoreInAppBrowserOpenOptions): InAppBrowserObject { - options = options || {}; - options.usewkwebview = 'yes'; // Force WKWebView in iOS. - options.enableViewPortScale = options.enableViewPortScale ?? 'yes'; // Enable zoom on iOS by default. - options.allowInlineMediaPlayback = options.allowInlineMediaPlayback ?? 'yes'; // Allow playing inline videos in iOS. - - if (!options.location && CorePlatform.isIOS() && url.indexOf('file://') === 0) { - // The URL uses file protocol, don't show it on iOS. - // In Android we keep it because otherwise we lose the whole toolbar. - options.location = 'no'; - } - - CoreInAppBrowser.setInAppBrowserToolbarColors(options); - - CoreInAppBrowser.iabInstance = InAppBrowser.create(url, '_blank', options); - - if (CorePlatform.isMobile()) { - const loadStartUrls: string[] = []; - - const loadStartSubscription = CoreInAppBrowser.iabInstance.on('loadstart').subscribe((event) => { - NgZone.run(() => { - // Store the last loaded URLs (max 10). - loadStartUrls.push(event.url); - if (loadStartUrls.length > 10) { - loadStartUrls.shift(); - } - - CoreEvents.trigger(CoreEvents.IAB_LOAD_START, event); - }); - }); - - const loadStopSubscription = CoreInAppBrowser.iabInstance.on('loadstop').subscribe((event) => { - NgZone.run(() => { - CoreEvents.trigger(CoreEvents.IAB_LOAD_STOP, event); - }); - }); - - const messageSubscription = CoreInAppBrowser.iabInstance.on('message').subscribe((event) => { - NgZone.run(() => { - CoreEvents.trigger(CoreEvents.IAB_MESSAGE, event.data); - }); - }); - - const exitSubscription = CoreInAppBrowser.iabInstance.on('exit').subscribe((event) => { - NgZone.run(() => { - loadStartSubscription.unsubscribe(); - loadStopSubscription.unsubscribe(); - messageSubscription.unsubscribe(); - exitSubscription.unsubscribe(); - - CoreInAppBrowser.iabInstance = undefined; - CoreEvents.trigger(CoreEvents.IAB_EXIT, event); - }); - }); - } - - CoreAnalytics.logEvent({ - type: CoreAnalyticsEventType.OPEN_LINK, - link: CoreUrl.unfixPluginfileURL(options.originalUrl ?? url), - }); - - return CoreInAppBrowser.iabInstance; - } - - /** - * Given some IAB options, set the toolbar colors properties to the right values. - * - * @param options Options to change. - * @returns Changed options. - */ - protected static setInAppBrowserToolbarColors(options: InAppBrowserOptions): InAppBrowserOptions { - if (options.toolbarcolor) { - // Color already set. - return options; - } - - // Color not set. Check if it needs to be changed automatically. - let bgColor: string | undefined; - let textColor: string | undefined; - - if (CoreConstants.CONFIG.iabToolbarColors === 'auto') { - bgColor = CoreColors.getToolbarBackgroundColor(); - } else if (CoreConstants.CONFIG.iabToolbarColors && typeof CoreConstants.CONFIG.iabToolbarColors === 'object') { - bgColor = CoreConstants.CONFIG.iabToolbarColors.background; - textColor = CoreConstants.CONFIG.iabToolbarColors.text; - } - - if (!bgColor) { - // Use default color. In iOS, use black background color since the default is transparent and doesn't look good. - options.locationcolor = '#000000'; - - return options; - } - - if (!textColor) { - textColor = CoreColors.isWhiteContrastingBetter(bgColor) ? '#ffffff' : '#000000'; - } - - options.toolbarcolor = bgColor; - options.closebuttoncolor = textColor; - options.navigationbuttoncolor = textColor; - options.locationcolor = bgColor; - options.locationtextcolor = textColor; - - return options; - } - -} - -/** - * Options for opening in InAppBrowser. - */ -export type CoreInAppBrowserOpenOptions = InAppBrowserOptions & { - originalUrl?: string; // Original URL to open (in case the URL was treated, e.g. to add a token or an auto-login). -}; diff --git a/src/core/singletons/opener.ts b/src/core/singletons/opener.ts new file mode 100644 index 000000000..da0233182 --- /dev/null +++ b/src/core/singletons/opener.ts @@ -0,0 +1,433 @@ +// (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 { InAppBrowserObject, InAppBrowserOptions } from '@awesome-cordova-plugins/in-app-browser'; +import { CoreErrorWithOptions } from '@classes/errors/errorwithoptions'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreFilepool } from '@services/filepool'; +import { CoreLang, CoreLangFormat } from '@services/lang'; +import { CorePlatform } from '@services/platform'; +import { CoreSites } from '@services/sites'; +import { CoreMimetypeUtils } from '@services/utils/mimetype'; +import { Translate, FileOpener, WebIntent, InAppBrowser, NgZone } from '@singletons'; +import { CoreConstants } from '../constants'; +import { CoreFile } from '@services/file'; +import { CorePromiseUtils } from './promise-utils'; +import { CoreUrl } from './url'; +import { CoreLogger } from './logger'; +import { CoreConfig } from '@services/config'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreEvents } from '@singletons/events'; +import { CoreColors } from './colors'; + +/** + * Singleton with helper functions to handler open files and urls. + */ +export class CoreOpener { + + protected static logger = CoreLogger.getInstance('CoreOpener'); + + // Avoid creating singleton instances. + private constructor() { + // Nothing to do. + } + + /** + * Show a confirm before opening a link in browser, unless the user previously marked to not show again. + * + * @param url URL to open. + */ + protected static async confirmOpenBrowserIfNeeded(url: string): Promise { + if (!CoreUrl.isHttpURL(url)) { + // Only ask confirm for http(s), other cases usually launch external apps. + return; + } + + // Check if the user decided not to see the warning. + const dontShowWarning = await CoreConfig.get(CoreConstants.SETTINGS_DONT_SHOW_EXTERNAL_LINK_WARN, 0); + if (dontShowWarning) { + return; + } + + // Remove common sensitive information from the URL. + url = url + .replace(/token=[^&#]+/gi, 'token=secret') + .replace(/tokenpluginfile\.php\/[^/]+/gi, 'tokenpluginfile.php/secret'); + + const dontShowAgain = await CoreDomUtils.showPrompt( + Translate.instant('core.warnopeninbrowser', { url }), + undefined, + Translate.instant('core.dontshowagain'), + 'checkbox', + ); + + if (dontShowAgain) { + CoreConfig.set(CoreConstants.SETTINGS_DONT_SHOW_EXTERNAL_LINK_WARN, 1); + } + } + + /** + * Open a file using platform specific method. + * + * @param path The local path of the file to be open. + * @param options Options. + * @returns Promise resolved when done. + */ + static async openFile(path: string, options: CoreOpenerOpenFileOptions = {}): Promise { + // Convert the path to a native path if needed. + path = CoreFile.unconvertFileSrc(path); + + const extension = CoreMimetypeUtils.getFileExtension(path); + const mimetype = extension && CoreMimetypeUtils.getMimeType(extension); + + if (mimetype == 'text/html' && CorePlatform.isAndroid()) { + // Open HTML local files in InAppBrowser, in system browser some embedded files aren't loaded. + CoreOpener.openInApp(path); + + return; + } else if (extension === 'apk' && CorePlatform.isAndroid()) { + const url = await CorePromiseUtils.ignoreErrors( + CoreFilepool.getFileUrlByPath(CoreSites.getCurrentSiteId(), CoreFile.removeBasePath(path)), + ); + + // @todo MOBILE-4167: Handle urls with expired tokens. + + throw new CoreErrorWithOptions( + Translate.instant('core.cannotinstallapkinfo'), + Translate.instant('core.cannotinstallapk'), + url + ? [ + { + text: Translate.instant('core.openinbrowser'), + handler: () => CoreOpener.openInBrowser(url), + }, + { + text: Translate.instant('core.cancel'), + role: 'cancel', + }, + ] + : undefined, + ); + } + + // Path needs to be decoded, the file won't be opened if the path has %20 instead of spaces and so. + try { + path = decodeURIComponent(path); + } catch { + // Error, use the original path. + } + + const openFile = async (mimetype?: string) => { + if (CoreOpener.shouldOpenWithDialog(options)) { + await FileOpener.showOpenWithDialog(path, mimetype || ''); + } else { + await FileOpener.open(path, mimetype || ''); + } + }; + + try { + try { + await openFile(mimetype); + } catch (error) { + if (!extension || !error || Number(error.status) !== 9) { + throw error; + } + + // Cannot open mimetype. Check if there is a deprecated mimetype for the extension. + const deprecatedMimetype = CoreMimetypeUtils.getDeprecatedMimeType(extension); + if (!deprecatedMimetype || deprecatedMimetype === mimetype) { + throw error; + } + + await openFile(deprecatedMimetype); + } + } catch (error) { + CoreOpener.logger.error('Error opening file ' + path + ' with mimetype ' + mimetype); + CoreOpener.logger.error('Error: ', JSON.stringify(error)); + + if (!extension || extension.indexOf('/') > -1 || extension.indexOf('\\') > -1) { + // Extension not found. + throw new Error(Translate.instant('core.erroropenfilenoextension')); + } + + throw new Error(Translate.instant('core.erroropenfilenoapp')); + } + } + + /** + * Open a URL using a browser. + * Do not use for files, refer to {@link CoreOpener.openFile}. + * + * @param url The URL to open. + * @param options Options. + */ + static async openInBrowser(url: string, options: CoreOpenerOpenInBrowserOptions = {}): Promise { + // eslint-disable-next-line deprecation/deprecation + const originaUrl = CoreUrl.unfixPluginfileURL(options.originalUrl ?? options.browserWarningUrl ?? url); + if (options.showBrowserWarning || options.showBrowserWarning === undefined) { + try { + await CoreOpener.confirmOpenBrowserIfNeeded(originaUrl); + } catch { + // Cancelled, stop. + return; + } + } + + const site = CoreSites.getCurrentSite(); + CoreAnalytics.logEvent({ type: CoreAnalyticsEventType.OPEN_LINK, link: originaUrl }); + window.open( + site?.containsUrl(url) + ? CoreUrl.addParamsToUrl(url, { lang: await CoreLang.getCurrentLanguage(CoreLangFormat.LMS) }) + : url, + '_system', + ); + } + + /** + * Open an online file using platform specific method. + * Specially useful for audio and video since they can be streamed. + * + * @param url The URL of the file. + * @returns Promise resolved when opened. + */ + static async openOnlineFile(url: string): Promise { + if (CorePlatform.isAndroid()) { + // In Android we need the mimetype to open it. + const mimetype = await CorePromiseUtils.ignoreErrors(CoreMimetypeUtils.getMimeTypeFromUrl(url)); + + if (!mimetype) { + // Couldn't retrieve mimetype. Return error. + throw new Error(Translate.instant('core.erroropenfilenoextension')); + } + + const options = { + action: WebIntent.ACTION_VIEW, + url, + type: mimetype, + }; + + try { + await WebIntent.startActivity(options); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.OPEN_LINK, + link: CoreUrl.unfixPluginfileURL(url), + }); + + return; + } catch (error) { + CoreOpener.logger.error('Error opening online file ' + url + ' with mimetype ' + mimetype); + CoreOpener.logger.error('Error: ', JSON.stringify(error)); + + throw new Error(Translate.instant('core.erroropenfilenoapp')); + } + } + + // In the rest of platforms we need to open them in InAppBrowser. + CoreOpener.openInApp(url); + } + + /** + * Given some options, check if a file should be opened with showOpenWithDialog. + * + * @param options Options. + * @returns Boolean. + */ + static shouldOpenWithDialog(options: CoreOpenerOpenFileOptions = {}): boolean { + const openFileAction = options.iOSOpenFileAction ?? CoreConstants.CONFIG.iOSDefaultOpenFileAction; + + return CorePlatform.isIOS() && openFileAction == OpenFileAction.OPEN_WITH; + } + + private static iabInstance?: InAppBrowserObject; + + /** + * Close the InAppBrowser window. + */ + static closeInAppBrowser(): void { + if (!CoreOpener.iabInstance) { + return; + } + + CoreOpener.iabInstance.close(); + } + + /** + * Get inapp browser instance (if any). + * + * @returns IAB instance, undefined if not open. + */ + static getInAppBrowserInstance(): InAppBrowserObject | undefined { + return CoreOpener.iabInstance; + } + + /** + * Check if inapp browser is open. + * + * @returns Whether it's open. + */ + static isInAppBrowserOpen(): boolean { + return !!CoreOpener.iabInstance; + } + + /** + * Open a URL using InAppBrowser. + * Do not use for files, refer to CoreOpener.openFile. + * + * @param url The URL to open. + * @param options Override default options passed to InAppBrowser. + * @returns The opened window. + */ + static openInApp(url: string, options?: CoreOpenerOpenInAppBrowserOptions): InAppBrowserObject { + options = options || {}; + options.usewkwebview = 'yes'; // Force WKWebView in iOS. + options.enableViewPortScale = options.enableViewPortScale ?? 'yes'; // Enable zoom on iOS by default. + options.allowInlineMediaPlayback = options.allowInlineMediaPlayback ?? 'yes'; // Allow playing inline videos in iOS. + + if (!options.location && CorePlatform.isIOS() && url.indexOf('file://') === 0) { + // The URL uses file protocol, don't show it on iOS. + // In Android we keep it because otherwise we lose the whole toolbar. + options.location = 'no'; + } + + CoreOpener.setInAppBrowserToolbarColors(options); + + CoreOpener.iabInstance = InAppBrowser.create(url, '_blank', options); + + if (CorePlatform.isMobile()) { + const loadStartUrls: string[] = []; + + const loadStartSubscription = CoreOpener.iabInstance.on('loadstart').subscribe((event) => { + NgZone.run(() => { + // Store the last loaded URLs (max 10). + loadStartUrls.push(event.url); + if (loadStartUrls.length > 10) { + loadStartUrls.shift(); + } + + CoreEvents.trigger(CoreEvents.IAB_LOAD_START, event); + }); + }); + + const loadStopSubscription = CoreOpener.iabInstance.on('loadstop').subscribe((event) => { + NgZone.run(() => { + CoreEvents.trigger(CoreEvents.IAB_LOAD_STOP, event); + }); + }); + + const messageSubscription = CoreOpener.iabInstance.on('message').subscribe((event) => { + NgZone.run(() => { + CoreEvents.trigger(CoreEvents.IAB_MESSAGE, event.data); + }); + }); + + const exitSubscription = CoreOpener.iabInstance.on('exit').subscribe((event) => { + NgZone.run(() => { + loadStartSubscription.unsubscribe(); + loadStopSubscription.unsubscribe(); + messageSubscription.unsubscribe(); + exitSubscription.unsubscribe(); + + CoreOpener.iabInstance = undefined; + CoreEvents.trigger(CoreEvents.IAB_EXIT, event); + }); + }); + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.OPEN_LINK, + link: CoreUrl.unfixPluginfileURL(options.originalUrl ?? url), + }); + + return CoreOpener.iabInstance; + } + + /** + * Given some IAB options, set the toolbar colors properties to the right values. + * + * @param options Options to change. + * @returns Changed options. + */ + protected static setInAppBrowserToolbarColors(options: InAppBrowserOptions): InAppBrowserOptions { + if (options.toolbarcolor) { + // Color already set. + return options; + } + + // Color not set. Check if it needs to be changed automatically. + let bgColor: string | undefined; + let textColor: string | undefined; + + if (CoreConstants.CONFIG.iabToolbarColors === 'auto') { + bgColor = CoreColors.getToolbarBackgroundColor(); + } else if (CoreConstants.CONFIG.iabToolbarColors && typeof CoreConstants.CONFIG.iabToolbarColors === 'object') { + bgColor = CoreConstants.CONFIG.iabToolbarColors.background; + textColor = CoreConstants.CONFIG.iabToolbarColors.text; + } + + if (!bgColor) { + // Use default color. In iOS, use black background color since the default is transparent and doesn't look good. + options.locationcolor = '#000000'; + + return options; + } + + if (!textColor) { + textColor = CoreColors.isWhiteContrastingBetter(bgColor) ? '#ffffff' : '#000000'; + } + + options.toolbarcolor = bgColor; + options.closebuttoncolor = textColor; + options.navigationbuttoncolor = textColor; + options.locationcolor = bgColor; + options.locationtextcolor = textColor; + + return options; + } + +} + +/** + * Options for opening in InAppBrowser. + */ +export type CoreOpenerOpenInAppBrowserOptions = InAppBrowserOptions & { + originalUrl?: string; // Original URL to open (in case the URL was treated, e.g. to add a token or an auto-login). +}; + +/** + * Options for opening a file. + */ +export type CoreOpenerOpenFileOptions = { + iOSOpenFileAction?: OpenFileAction; // Action to do when opening a file. +}; + +/** + * Options for opening in browser. + */ +export type CoreOpenerOpenInBrowserOptions = { + showBrowserWarning?: boolean; // Whether to display a warning before opening in browser. Defaults to true. + originalUrl?: string; // Original URL to open (in case the URL was treated, e.g. to add a token or an auto-login). + /** + * @deprecated since 4.3. Use originalUrl instead. + */ + browserWarningUrl?: string; +}; + +/** + * Possible default picker actions. + */ +export enum OpenFileAction { + OPEN = 'open', + OPEN_WITH = 'open-with', +} diff --git a/src/core/singletons/window.ts b/src/core/singletons/window.ts index 6058d3723..03972fe60 100644 --- a/src/core/singletons/window.ts +++ b/src/core/singletons/window.ts @@ -13,15 +13,11 @@ // limitations under the License. import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; -import { CoreConfig } from '@services/config'; import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; -import { CoreDomUtils } from '@services/utils/dom'; import { CoreUrl } from '@singletons/url'; -import { CoreUtils } from '@services/utils/utils'; -import { Translate } from '@singletons'; -import { CoreConstants } from '../constants'; +import { CoreOpener } from './opener'; /** * Singleton with helper functions for windows. @@ -36,36 +32,16 @@ export class CoreWindow { /** * Show a confirm before opening a link in browser, unless the user previously marked to not show again. * - * @param url URL to open. - * @returns Promise resolved if confirmed, rejected if rejected. + * @returns Only shows a deprecation warning. + * @deprecated since 5.0. Not used anymore. Use CoreOpener.openInBrowser and it will confirm if needed. */ - static async confirmOpenBrowserIfNeeded(url: string): Promise { - if (!CoreUrl.isHttpURL(url)) { - // Only ask confirm for http(s), other cases usually launch external apps. - return; - } + static async confirmOpenBrowserIfNeeded(): Promise { + const { CoreLogger } = await import('@singletons/logger'); - // Check if the user decided not to see the warning. - const dontShowWarning = await CoreConfig.get(CoreConstants.SETTINGS_DONT_SHOW_EXTERNAL_LINK_WARN, 0); - if (dontShowWarning) { - return; - } + CoreLogger.getInstance('CoreWindow') + .warn('confirmOpenBrowserIfNeeded has been deprecated since 5.0. Not used anymore.\ + Use CoreOpener.openInBrowser and it will confirm if needed.'); - // Remove common sensitive information from the URL. - url = url - .replace(/token=[^&#]+/gi, 'token=secret') - .replace(/tokenpluginfile\.php\/[^/]+/gi, 'tokenpluginfile.php/secret'); - - const dontShowAgain = await CoreDomUtils.showPrompt( - Translate.instant('core.warnopeninbrowser', { url }), - undefined, - Translate.instant('core.dontshowagain'), - 'checkbox', - ); - - if (dontShowAgain) { - CoreConfig.set(CoreConstants.SETTINGS_DONT_SHOW_EXTERNAL_LINK_WARN, 1); - } } /** @@ -88,7 +64,7 @@ export class CoreWindow { } } - await CoreUtils.openFile(url); + await CoreOpener.openFile(url); } else { let treated = false; @@ -101,7 +77,7 @@ export class CoreWindow { // Not opened in the app, open with browser. Check if we need to auto-login. if (!CoreSites.isLoggedIn()) { // Not logged in, cannot auto-login. - CoreUtils.openInBrowser(url); + CoreOpener.openInBrowser(url); } else { await CoreSites.getRequiredCurrentSite().openInBrowserWithAutoLogin(url); }