diff --git a/src/core/features/contentlinks/services/contentlinks-delegate.ts b/src/core/features/contentlinks/services/contentlinks-delegate.ts index 2b3e1a93c..b83b4109e 100644 --- a/src/core/features/contentlinks/services/contentlinks-delegate.ts +++ b/src/core/features/contentlinks/services/contentlinks-delegate.ts @@ -216,16 +216,7 @@ export class CoreContentLinksDelegateService { } // Site is logged out, authenticate first before treating the URL. - const willReload = await CoreSites.logoutForRedirect(siteId, { - urlToOpen: url, - }); - - if (!willReload) { - // Load the site with the redirect data. - await CoreSites.loadSite(siteId, { - urlToOpen: url, - }); - } + await CoreSites.logout({ urlToOpen: url, siteId }); }; }); diff --git a/src/core/features/login/login.module.ts b/src/core/features/login/login.module.ts index b0d3d8fbd..8030f6411 100644 --- a/src/core/features/login/login.module.ts +++ b/src/core/features/login/login.module.ts @@ -41,6 +41,10 @@ const appRoutes: Routes = [ loadChildren: () => import('./login-lazy.module'), canActivate: [redirectGuard], }, + { + path: 'logout', + loadComponent: () => import('@features/login/pages/logout/logout'), + }, ]; @NgModule({ diff --git a/src/core/features/login/pages/logout/logout.html b/src/core/features/login/pages/logout/logout.html new file mode 100644 index 000000000..1e61e89a5 --- /dev/null +++ b/src/core/features/login/pages/logout/logout.html @@ -0,0 +1,3 @@ + + + diff --git a/src/core/features/login/pages/logout/logout.ts b/src/core/features/login/pages/logout/logout.ts new file mode 100644 index 000000000..db0bc10ff --- /dev/null +++ b/src/core/features/login/pages/logout/logout.ts @@ -0,0 +1,104 @@ +// (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 { Component, OnInit } from '@angular/core'; +import { CoreSites } from '@services/sites'; +import { CoreConstants } from '@/core/constants'; +import { CoreNavigationOptions, CoreNavigator, CoreRedirectPayload } from '@services/navigator'; +import { CoreSharedModule } from '@/core/shared.module'; +import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; +import { CoreRedirects } from '@singletons/redirects'; + +/** + * Page that logs the user out. + */ +@Component({ + selector: 'page-core-login-logout', + templateUrl: 'logout.html', + standalone: true, + imports: [ + CoreSharedModule, + ], +}) +export default class CoreLoginLogoutPage implements OnInit { + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + const siteId = CoreNavigator.getRouteParam('siteId') ?? CoreConstants.NO_SITE_ID; + const logoutOptions = { + forceLogout: CoreNavigator.getRouteBooleanParam('forceLogout'), + removeAccount: CoreNavigator.getRouteBooleanParam('removeAccount') ?? !!CoreConstants.CONFIG.removeaccountonlogout, + }; + const redirectData = { + redirectPath: CoreNavigator.getRouteParam('redirectPath'), + redirectOptions: CoreNavigator.getRouteParam('redirectOptions'), + urlToOpen: CoreNavigator.getRouteParam('urlToOpen'), + }; + + if (!CoreSites.isLoggedIn()) { + // This page shouldn't open if user isn't logged in, but if that happens just navigate to the right page. + await this.navigateAfterLogout(siteId, redirectData); + + return; + } + + const shouldReload = CoreSitePlugins.hasSitePluginsLoaded; + if (shouldReload && (siteId !== CoreConstants.NO_SITE_ID || redirectData.redirectPath || redirectData.urlToOpen)) { + // The app will reload and we need to open a page that isn't the default page. Store the redirect first. + CoreRedirects.storeRedirect(siteId, redirectData); + } + + await CoreSites.internalLogout(logoutOptions); + + if (shouldReload) { + // We need to reload the app to unload all the plugins. Leave the logout page first. + await CoreNavigator.navigate('/login', { reset: true }); + + window.location.reload(); + + return; + } + + await this.navigateAfterLogout(siteId, redirectData); + } + + /** + * Navigate to the right page after logout is done. + * + * @param siteId Site ID to load. + * @param redirectData Redirect data. + */ + protected async navigateAfterLogout(siteId: string, redirectData: CoreRedirectPayload): Promise { + if (siteId === CoreConstants.NO_SITE_ID) { + // No site to load now, just navigate. + await CoreNavigator.navigate(redirectData.redirectPath ?? '/login/sites', { + ...redirectData.redirectOptions, + reset: true, + }); + + return; + } + + // Load the site and navigate. + const loggedIn = await CoreSites.loadSite(siteId, redirectData); + if (!loggedIn) { + return; // Session expired. + } + + await CoreNavigator.navigateToSiteHome({ params: redirectData, preferCurrentTab: false, siteId }); + } + +} diff --git a/src/core/features/login/services/login-helper.ts b/src/core/features/login/services/login-helper.ts index 9be527c82..23ee93791 100644 --- a/src/core/features/login/services/login-helper.ts +++ b/src/core/features/login/services/login-helper.ts @@ -412,22 +412,19 @@ export class CoreLoginHelperProvider { * @returns Promise resolved when done. */ async goToAddSite(setRoot = false, showKeyboard = false): Promise { - let path = '/login/sites'; - let params: Params = { openAddSite: true , showKeyboard }; - if (CoreSites.isLoggedIn()) { - const willReload = await CoreSites.logoutForRedirect(CoreConstants.NO_SITE_ID, { - redirectPath: path, - redirectOptions: { params }, + // Logout first. + await CoreSites.logout({ + siteId: CoreConstants.NO_SITE_ID, + redirectPath: '/login/sites', + redirectOptions: { params: { openAddSite: true , showKeyboard } }, }); - if (willReload) { - return; - } - } else { - [path, params] = await this.getAddSiteRouteInfo(showKeyboard); + return; } + const [path, params] = await this.getAddSiteRouteInfo(showKeyboard); + await CoreNavigator.navigate(path, { params, reset: setRoot }); } diff --git a/src/core/features/mainmenu/components/user-menu/user-menu.ts b/src/core/features/mainmenu/components/user-menu/user-menu.ts index e52071150..1aecec73f 100644 --- a/src/core/features/mainmenu/components/user-menu/user-menu.ts +++ b/src/core/features/mainmenu/components/user-menu/user-menu.ts @@ -18,7 +18,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { CoreSite } from '@classes/sites/site'; import { CoreSiteInfo } from '@classes/sites/unauthenticated-site'; import { CoreFilter } from '@features/filter/services/filter'; -import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/support/authenticated-support-config'; import { CoreUserSupport } from '@features/user/services/support'; import { CoreUser, CoreUserProfile } from '@features/user/services/user'; @@ -33,8 +32,9 @@ import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CorePromiseUtils } from '@singletons/promise-utils'; -import { ModalController, Translate } from '@singletons'; +import { ModalController } from '@singletons'; import { Subscription } from 'rxjs'; +import { CoreLoginHelper } from '@features/login/services/login-helper'; /** * Component to display a user menu. @@ -208,12 +208,6 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy { * @param event Click event */ async logout(event: Event): Promise { - if (CoreNavigator.currentRouteCanBlockLeave()) { - await CoreDomUtils.showAlert(undefined, Translate.instant('core.cannotlogoutpageblocks')); - - return; - } - if (this.removeAccountOnLogout) { // Ask confirm. const siteName = this.siteName ? @@ -242,12 +236,6 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy { * @param event Click event */ async switchAccounts(event: Event): Promise { - if (CoreNavigator.currentRouteCanBlockLeave()) { - await CoreDomUtils.showAlert(undefined, Translate.instant('core.cannotlogoutpageblocks')); - - return; - } - const thisModal = await ModalController.getTop(); event.preventDefault(); 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 94be08c61..dfab2c14f 100644 --- a/src/core/features/policy/pages/site-policy/site-policy.ts +++ b/src/core/features/policy/pages/site-policy/site-policy.ts @@ -280,9 +280,7 @@ export class CorePolicySitePolicyPage implements OnInit, OnDestroy { * @returns Promise resolved when done. */ async cancel(): Promise { - await CorePromiseUtils.ignoreErrors(CoreSites.logout()); - - await CoreNavigator.navigate('/login/sites', { reset: true }); + await CoreSites.logout(); } /** diff --git a/src/core/features/sitehome/tests/links.test.ts b/src/core/features/sitehome/tests/links.test.ts index f137e69ec..cf4b21326 100644 --- a/src/core/features/sitehome/tests/links.test.ts +++ b/src/core/features/sitehome/tests/links.test.ts @@ -32,6 +32,7 @@ describe('Site Home link handlers', () => { isStoredRootURL: () => Promise.resolve({ siteIds: [siteId] }), getSite: () => Promise.resolve(new CoreSite(siteId, siteUrl, '')), getSiteIdsFromUrl: () => Promise.resolve([siteId]), + getCurrentSiteId: () => siteId, })); mockSingleton(CoreLoginHelper, { getAvailableSites: async () => [{ url: siteUrl, name: 'Example Campus' }] }); diff --git a/src/core/services/navigator.ts b/src/core/services/navigator.ts index 6632359f7..2d07de2d3 100644 --- a/src/core/services/navigator.ts +++ b/src/core/services/navigator.ts @@ -220,14 +220,13 @@ export class CoreNavigatorService { // If we are logged into a different site, log out first. if (CoreSites.isLoggedIn() && CoreSites.getCurrentSiteId() !== siteId) { - const willReload = await CoreSites.logoutForRedirect(siteId, { + await CoreSites.logout({ redirectPath: path, redirectOptions: options || {}, + siteId, }); - if (willReload) { - return true; - } + return true; } // If the path doesn't belong to a site, call standard navigation. diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index 1dfdd725e..b761cc9f6 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -141,14 +141,6 @@ export class CoreSitesProvider { // Remove version classes from body. CoreHTMLClasses.removeSiteClasses(); - - // Go to sites page when user is logged out. - await CoreNavigator.navigate('/login/sites', { reset: true }); - - if (CoreSitePlugins.hasSitePluginsLoaded) { - // Temporary fix. Reload the page to unload all plugins. - window.location.reload(); - } }); CoreEvents.on(CoreEvents.LOGIN, async (data) => { @@ -964,7 +956,7 @@ export class CoreSitesProvider { promise.finally(() => { if (siteId) { // Logout the currentSite and expire the token. - this.logout(); + this.internalLogout(); this.setSiteLoggedOut(siteId); } }); @@ -1123,7 +1115,7 @@ export class CoreSitesProvider { this.logger.debug(`Delete site ${siteId}`); if (this.currentSite !== undefined && this.currentSite.id == siteId) { - this.logout(); + this.internalLogout(); } const site = await this.getSite(siteId); @@ -1457,10 +1449,23 @@ export class CoreSitesProvider { /** * Logout the user. * - * @param options Logout options. - * @returns Promise resolved when the user is logged out. + * @param options Options. */ async logout(options: CoreSitesLogoutOptions = {}): Promise { + await CoreNavigator.navigate('/logout', { + params: { ...options }, + reset: true, + }); + } + + /** + * Logout the user. + * This function is for internal usage, please use CoreSites.logout instead. The reason this function is public is because + * it's called from the CoreLoginLogoutPage page. + * + * @param options Logout options. + */ + async internalLogout(options: InternalLogoutOptions = {}): Promise { if (!this.currentSite) { return; } @@ -1494,6 +1499,7 @@ export class CoreSitesProvider { * @param siteId Site that will be opened after logout. * @param redirectData Page/url to open after logout. * @returns Promise resolved with boolean: true if app will be reloaded after logout. + * @deprecated since 5.0. Use CoreSites.logout instead, it automatically handles redirects. */ async logoutForRedirect(siteId: string, redirectData: CoreRedirectPayload): Promise { if (!this.currentSite) { @@ -1505,7 +1511,7 @@ export class CoreSitesProvider { CoreRedirects.storeRedirect(siteId, redirectData); } - await this.logout(); + await this.internalLogout(); return CoreSitePlugins.hasSitePluginsLoaded; } @@ -2480,7 +2486,14 @@ export type CoreSitesLoginTokenResponse = { /** * Options for logout. */ -export type CoreSitesLogoutOptions = { +export type CoreSitesLogoutOptions = CoreRedirectPayload & InternalLogoutOptions & { + siteId?: string; // Site ID to load after logout. +}; + +/** + * Options for internal logout. + */ +type InternalLogoutOptions = { forceLogout?: boolean; // If true, site will be marked as logged out, no matter the value tool_mobile_forcelogout. removeAccount?: boolean; // If true, site will be removed too after logout. }; diff --git a/src/core/services/tests/sites.test.ts b/src/core/services/tests/sites.test.ts index 68e477405..961c84708 100644 --- a/src/core/services/tests/sites.test.ts +++ b/src/core/services/tests/sites.test.ts @@ -16,7 +16,6 @@ import { CoreEvents } from '@singletons/events'; import { CoreLang, CoreLangProvider } from '@services/lang'; import { mock, mockSingleton } from '@/testing/utils'; -import { CoreNavigator, CoreNavigatorService } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { Http } from '@singletons'; import { of } from 'rxjs'; @@ -34,13 +33,10 @@ describe('CoreSitesProvider', () => { }); it('cleans up on logout', async () => { - const navigator: CoreNavigatorService = mockSingleton(CoreNavigator, ['navigate']); - CoreSites.initialize(); CoreEvents.trigger(CoreEvents.LOGOUT); expect(langProvider.clearCustomStrings).toHaveBeenCalled(); - expect(navigator.navigate).toHaveBeenCalledWith('/login/sites', { reset: true }); }); it('adds ionic platform and theme classes', async () => { diff --git a/src/core/services/urlschemes.ts b/src/core/services/urlschemes.ts index 65cd87bc4..833bdcc8e 100644 --- a/src/core/services/urlschemes.ts +++ b/src/core/services/urlschemes.ts @@ -432,14 +432,13 @@ export class CoreCustomURLSchemesProvider { // Ask the user before changing site. await CoreDomUtils.showConfirm(Translate.instant('core.contentlinks.confirmurlothersite')); - const willReload = await CoreSites.logoutForRedirect(CoreConstants.NO_SITE_ID, { + await CoreSites.logout({ + siteId: CoreConstants.NO_SITE_ID, redirectPath: '/login/credentials', redirectOptions: { params: pageParams }, }); - if (willReload) { - return; - } + return; } await CoreNavigator.navigateToLoginCredentials(pageParams); diff --git a/upgrade.txt b/upgrade.txt index edb6425e0..a9429f01f 100644 --- a/upgrade.txt +++ b/upgrade.txt @@ -2,6 +2,10 @@ This file describes API changes in the Moodle App that affect site plugins, info For more information about upgrading, read the official documentation: https://moodledev.io/general/app/upgrading/ +=== 5.0.0 === + + - The logout process has been refactored, now it uses a logout page to trigger Angular guards. CoreSites.logout now uses this process, and CoreSites.logoutForRedirect is deprecated and shouldn't be used anymore. + === 4.5.0 === - Ionic has been upgraded to major version 8. See breaking changes and upgrade guide here: https://ionicframework.com/docs/updating/8-0