From ef2f68a5fdd3fd71a1ddf84c8416e9567309b12d Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Tue, 27 Apr 2021 10:55:43 +0200
Subject: [PATCH] MOBILE-3742 core: Support redirect to another site

---
 src/app/app-routing.module.ts                 | 12 +--
 .../services/contentlinks-helper.ts           | 12 ++-
 .../course/components/format/format.ts        |  7 +-
 .../features/course/pages/index/index.html    |  2 +-
 .../features/course/pages/index/index.page.ts | 23 ++++--
 src/core/features/login/login.module.ts       |  3 +
 .../features/mainmenu/pages/home/home.html    |  3 +-
 src/core/features/mainmenu/pages/home/home.ts | 75 +++++++++++++++++++
 src/core/features/mainmenu/pages/menu/menu.ts | 70 -----------------
 src/core/guards/redirect.ts                   | 62 +++++++--------
 src/core/services/app.ts                      | 15 +++-
 src/core/services/navigator.ts                | 10 ++-
 12 files changed, 165 insertions(+), 129 deletions(-)

diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index 61b44dd53..937bc5166 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -26,7 +26,6 @@ import {
 } from '@angular/router';
 
 import { CoreArray } from '@singletons/array';
-import { CoreRedirectGuard } from '@guards/redirect';
 
 /**
  * Build app routes.
@@ -35,16 +34,7 @@ import { CoreRedirectGuard } from '@guards/redirect';
  * @return App routes.
  */
 function buildAppRoutes(injector: Injector): Routes {
-    const appRoutes = CoreArray.flatten(injector.get<Routes[]>(APP_ROUTES, []));
-
-    return appRoutes.map(route => {
-        route.canLoad = route.canLoad ?? [];
-        route.canActivate = route.canActivate ?? [];
-        route.canLoad.push(CoreRedirectGuard);
-        route.canActivate.push(CoreRedirectGuard);
-
-        return route;
-    });
+    return CoreArray.flatten(injector.get<Routes[]>(APP_ROUTES, []));
 }
 
 /**
diff --git a/src/core/features/contentlinks/services/contentlinks-helper.ts b/src/core/features/contentlinks/services/contentlinks-helper.ts
index 6e7d52cfd..306ac53d5 100644
--- a/src/core/features/contentlinks/services/contentlinks-helper.ts
+++ b/src/core/features/contentlinks/services/contentlinks-helper.ts
@@ -56,7 +56,7 @@ export class CoreContentLinksHelperProvider {
     }
 
     /**
-     * Get the first valid action in the list of possible actions to do for a URL.
+     * Get the first valid action for a URL.
      *
      * @param url URL to handle.
      * @param courseId Course ID related to the URL. Optional but recommended.
@@ -75,6 +75,16 @@ export class CoreContentLinksHelperProvider {
             return;
         }
 
+        return this.getFirstValidAction(actions);
+    }
+
+    /**
+     * Get the first valid action in a list of possible actions.
+     *
+     * @param actions Actions.
+     * @return First valid action if any.
+     */
+    getFirstValidAction(actions: CoreContentLinksAction[]): CoreContentLinksAction | undefined {
         return actions.find((action) => action && action.sites && action.sites.length);
     }
 
diff --git a/src/core/features/course/components/format/format.ts b/src/core/features/course/components/format/format.ts
index 5fb0b0c53..2187defbd 100644
--- a/src/core/features/course/components/format/format.ts
+++ b/src/core/features/course/components/format/format.ts
@@ -311,11 +311,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
             this.sectionChanged(sections[0]);
         } else if (this.initialSectionId || this.initialSectionNumber) {
             // We have an input indicating the section ID to load. Search the section.
-            const section = sections.find((section) => {
-                if (section.id != this.initialSectionId && (!section.section || section.section != this.initialSectionNumber)) {
-                    return false;
-                }
-            });
+            const section = sections.find((section) =>
+                section.id == this.initialSectionId || (section.section && section.section == this.initialSectionNumber));
 
             // Don't load the section if it cannot be viewed by the user.
             if (section && this.canViewSection(section)) {
diff --git a/src/core/features/course/pages/index/index.html b/src/core/features/course/pages/index/index.html
index f436abd88..18d3d7dda 100644
--- a/src/core/features/course/pages/index/index.html
+++ b/src/core/features/course/pages/index/index.html
@@ -11,5 +11,5 @@
     </ion-toolbar>
 </ion-header>
 <ion-content>
-    <core-tabs-outlet [tabs]="tabs" [hideUntil]="loaded"></core-tabs-outlet>
+    <core-tabs-outlet [tabs]="tabs" [hideUntil]="loaded" (ionChange)="tabSelected()"></core-tabs-outlet>
 </ion-content>
diff --git a/src/core/features/course/pages/index/index.page.ts b/src/core/features/course/pages/index/index.page.ts
index 8629eb6a6..6ba057186 100644
--- a/src/core/features/course/pages/index/index.page.ts
+++ b/src/core/features/course/pages/index/index.page.ts
@@ -46,6 +46,8 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
     protected currentPagePath = '';
     protected selectTabObserver: CoreEventObserver;
     protected firstTabName?: string;
+    protected module?: CoreCourseWSModule;
+    protected modParams?: Params;
     protected contentsTab: CoreTabsOutletTab = {
         page: CONTENTS_PAGE_NAME,
         title: 'core.course.contents',
@@ -82,8 +84,8 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
         // Get params.
         this.course = CoreNavigator.getRouteParam('course');
         this.firstTabName = CoreNavigator.getRouteParam('selectedTab');
-        const module = CoreNavigator.getRouteParam<CoreCourseWSModule>('module');
-        const modParams = CoreNavigator.getRouteParam<Params>('modParams');
+        this.module = CoreNavigator.getRouteParam<CoreCourseWSModule>('module');
+        this.modParams = CoreNavigator.getRouteParam<Params>('modParams');
 
         this.currentPagePath = CoreNavigator.getCurrentPath();
         this.contentsTab.page = CoreTextUtils.concatenatePaths(this.currentPagePath, this.contentsTab.page);
@@ -93,9 +95,8 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
             sectionNumber: CoreNavigator.getRouteNumberParam('sectionNumber'),
         };
 
-        if (module) {
-            this.contentsTab.pageParams!.moduleId = module.id;
-            CoreCourseHelper.openModule(module, this.course!.id, this.contentsTab.pageParams!.sectionId, modParams);
+        if (this.module) {
+            this.contentsTab.pageParams!.moduleId = this.module.id;
         }
 
         this.tabs.push(this.contentsTab);
@@ -107,6 +108,18 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
         ]);
     }
 
+    /**
+     * A tab was selected.
+     */
+    tabSelected(): void {
+        if (this.module) {
+            // Now that the first tab has been selected we can load the module.
+            CoreCourseHelper.openModule(this.module, this.course!.id, this.contentsTab.pageParams!.sectionId, this.modParams);
+
+            delete this.module;
+        }
+    }
+
     /**
      * Load course option handlers.
      *
diff --git a/src/core/features/login/login.module.ts b/src/core/features/login/login.module.ts
index 8525d25b7..75a8e77c7 100644
--- a/src/core/features/login/login.module.ts
+++ b/src/core/features/login/login.module.ts
@@ -17,6 +17,7 @@ import { Routes } from '@angular/router';
 
 import { AppRoutingModule } from '@/app/app-routing.module';
 import { CoreLoginHelperProvider } from './services/login-helper';
+import { CoreRedirectGuard } from '@guards/redirect';
 
 export const CORE_LOGIN_SERVICES = [
     CoreLoginHelperProvider,
@@ -26,6 +27,8 @@ const appRoutes: Routes = [
     {
         path: 'login',
         loadChildren: () => import('./login-lazy.module').then(m => m.CoreLoginLazyModule),
+        canActivate: [CoreRedirectGuard],
+        canLoad: [CoreRedirectGuard],
     },
 ];
 
diff --git a/src/core/features/mainmenu/pages/home/home.html b/src/core/features/mainmenu/pages/home/home.html
index 0605add72..1430ba018 100644
--- a/src/core/features/mainmenu/pages/home/home.html
+++ b/src/core/features/mainmenu/pages/home/home.html
@@ -14,7 +14,8 @@
 </ion-header>
 <ion-content>
     <core-loading [hideUntil]="loaded">
-        <core-tabs-outlet *ngIf="tabs.length > 0" [selectedIndex]="selectedTab" [hideUntil]="loaded" [tabs]="tabs">
+        <core-tabs-outlet *ngIf="tabs.length > 0" [selectedIndex]="selectedTab" [hideUntil]="loaded" [tabs]="tabs"
+            (ionChange)="tabSelected()">
         </core-tabs-outlet>
         <ng-container *ngIf="tabs.length == 0">
             <core-empty-box icon="fas-home" [message]="'core.courses.nocourses' | translate"></core-empty-box>
diff --git a/src/core/features/mainmenu/pages/home/home.ts b/src/core/features/mainmenu/pages/home/home.ts
index 0b1707bf3..50fe9f604 100644
--- a/src/core/features/mainmenu/pages/home/home.ts
+++ b/src/core/features/mainmenu/pages/home/home.ts
@@ -20,6 +20,12 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
 import { CoreTabsOutletComponent, CoreTabsOutletTab } from '@components/tabs-outlet/tabs-outlet';
 import { CoreMainMenuHomeDelegate, CoreMainMenuHomeHandlerToDisplay } from '../../services/home-delegate';
 import { CoreUtils } from '@services/utils/utils';
+import { ActivatedRoute } from '@angular/router';
+import { CoreNavigator, CoreRedirectPayload } from '@services/navigator';
+import { CoreCourseHelper } from '@features/course/services/course-helper';
+import { CoreCourse } from '@features/course/services/course';
+import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
+import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
 
 /**
  * Page that displays the Home.
@@ -40,11 +46,29 @@ export class CoreMainMenuHomePage implements OnInit {
 
     protected subscription?: Subscription;
     protected updateSiteObserver?: CoreEventObserver;
+    protected pendingRedirect?: CoreRedirectPayload;
+    protected urlToOpen?: string;
+
+    constructor(
+        protected route: ActivatedRoute,
+    ) {
+    }
 
     /**
      * Initialize the component.
      */
     ngOnInit(): void {
+        this.route.queryParams.subscribe((params: Partial<CoreRedirectPayload> & { urlToOpen?: string }) => {
+            this.urlToOpen = params.urlToOpen;
+
+            if (params.redirectPath) {
+                this.pendingRedirect = {
+                    redirectPath: params.redirectPath,
+                    redirectParams: params.redirectParams,
+                };
+            }
+        });
+
         this.loadSiteName();
 
         this.subscription = CoreMainMenuHomeDelegate.getHandlersObservable().subscribe((handlers) => {
@@ -111,6 +135,57 @@ export class CoreMainMenuHomePage implements OnInit {
         this.siteName = CoreSites.getCurrentSite()!.getSiteName();
     }
 
+    /**
+     * Handle a redirect.
+     *
+     * @param data Data received.
+     */
+    protected handleRedirect(data: CoreRedirectPayload): void {
+        const params = data.redirectParams;
+        const coursePathMatches = data.redirectPath.match(/^course\/(\d+)\/?$/);
+
+        if (coursePathMatches) {
+            if (!params?.course) {
+                CoreCourseHelper.getAndOpenCourse(Number(coursePathMatches[1]), params);
+            } else {
+                CoreCourse.openCourse(params.course, params);
+            }
+        } else {
+            CoreNavigator.navigateToSitePath(data.redirectPath, {
+                params: data.redirectParams,
+                preferCurrentTab: false,
+            });
+        }
+    }
+
+    /**
+     * Handle a URL to open.
+     *
+     * @param url URL to open.
+     */
+    protected async handleUrlToOpen(url: string): Promise<void> {
+        const actions = await CoreContentLinksDelegate.getActionsFor(url, undefined);
+
+        const action = CoreContentLinksHelper.getFirstValidAction(actions);
+        if (action) {
+            action.action(action.sites![0]);
+        }
+    }
+
+    /**
+     * Tab was selected.
+     */
+    tabSelected(): void {
+        if (this.pendingRedirect) {
+            this.handleRedirect(this.pendingRedirect);
+        } else if (this.urlToOpen) {
+            this.handleUrlToOpen(this.urlToOpen);
+        }
+
+        delete this.pendingRedirect;
+        delete this.urlToOpen;
+    }
+
     /**
      * User entered the page.
      */
diff --git a/src/core/features/mainmenu/pages/menu/menu.ts b/src/core/features/mainmenu/pages/menu/menu.ts
index 9dd61a115..3d2005371 100644
--- a/src/core/features/mainmenu/pages/menu/menu.ts
+++ b/src/core/features/mainmenu/pages/menu/menu.ts
@@ -19,14 +19,12 @@ import { BackButtonEvent } from '@ionic/core';
 import { Subscription } from 'rxjs';
 
 import { CoreApp } from '@services/app';
-import { CoreSites } from '@services/sites';
 import { CoreTextUtils } from '@services/utils/text';
 import { CoreEvents, CoreEventObserver } from '@singletons/events';
 import { CoreMainMenu, CoreMainMenuProvider } from '../../services/mainmenu';
 import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from '../../services/mainmenu-delegate';
 import { CoreDomUtils } from '@services/utils/dom';
 import { Translate } from '@singletons';
-import { CoreNavigator, CoreRedirectPayload } from '@services/navigator';
 
 /**
  * Page that displays the main menu of the app.
@@ -47,9 +45,6 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
     morePageName = CoreMainMenuProvider.MORE_PAGE_NAME;
 
     protected subscription?: Subscription;
-    protected redirectObs?: CoreEventObserver;
-    protected pendingRedirect?: CoreRedirectPayload;
-    protected urlToOpen?: string;
     protected keyboardObserver?: CoreEventObserver;
     protected resizeFunction: () => void;
     protected backButtonFunction: (event: BackButtonEvent) => void;
@@ -72,24 +67,6 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
      * Initialize the component.
      */
     ngOnInit(): void {
-        // @TODO this should be handled by route guards and can be removed
-        if (!CoreSites.isLoggedIn()) {
-            CoreNavigator.navigate('/login/init', { reset: true });
-
-            return;
-        }
-
-        this.route.queryParams.subscribe((params: Partial<CoreRedirectPayload> & { urlToOpen?: string }) => {
-            if (params.redirectPath) {
-                this.pendingRedirect = {
-                    redirectPath: params.redirectPath,
-                    redirectParams: params.redirectParams,
-                };
-            }
-
-            this.urlToOpen = params.urlToOpen;
-        });
-
         this.showTabs = true;
 
         this.subscription = CoreMainMenuDelegate.getHandlersObservable().subscribe((handlers) => {
@@ -97,16 +74,6 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
             this.allHandlers = handlers.filter((handler) => !handler.onlyInMore);
 
             this.initHandlers();
-
-            if (this.loaded && this.pendingRedirect) {
-                // Wait for tabs to be initialized and then handle the redirect.
-                setTimeout(() => {
-                    if (this.pendingRedirect) {
-                        this.handleRedirect(this.pendingRedirect);
-                        delete this.pendingRedirect;
-                    }
-                });
-            }
         });
 
         window.addEventListener('resize', this.resizeFunction);
@@ -158,18 +125,6 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
             this.tabs.sort((a, b) => (b.priority || 0) - (a.priority || 0));
 
             this.loaded = CoreMainMenuDelegate.areHandlersLoaded();
-
-            if (this.loaded && this.mainTabs && !this.mainTabs.getSelected()) {
-                // Select the first tab.
-                setTimeout(() => {
-                    this.mainTabs!.select(this.tabs[0]?.page || this.morePageName);
-                });
-            }
-        }
-
-        if (this.urlToOpen) {
-            // There's a content link to open.
-            // @todo: Treat URL.
         }
     }
 
@@ -189,36 +144,11 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
         }
     }
 
-    /**
-     * Handle a redirect.
-     *
-     * @param data Data received.
-     */
-    protected handleRedirect(data: CoreRedirectPayload): void {
-        // Check if the redirect page is the root page of any of the tabs.
-        const i = this.tabs.findIndex((tab) => tab.page == data.redirectPath);
-
-        if (i >= 0) {
-            // Tab found. Open it with the params.
-            CoreNavigator.navigate(data.redirectPath, {
-                params: data.redirectParams,
-                animated: false,
-            });
-        } else {
-            // Tab not found, use a phantom tab.
-            // @todo
-        }
-
-        // Force change detection, otherwise sometimes the tab was selected before the params were applied.
-        this.changeDetector.detectChanges();
-    }
-
     /**
      * Page destroyed.
      */
     ngOnDestroy(): void {
         this.subscription?.unsubscribe();
-        this.redirectObs?.off();
         window.removeEventListener('resize', this.resizeFunction);
         document.removeEventListener('ionBackButton', this.backButtonFunction);
         this.keyboardObserver?.off();
diff --git a/src/core/guards/redirect.ts b/src/core/guards/redirect.ts
index dad98c2fd..0f3fb750a 100644
--- a/src/core/guards/redirect.ts
+++ b/src/core/guards/redirect.ts
@@ -40,52 +40,48 @@ export class CoreRedirectGuard implements CanLoad, CanActivate {
      * Check if there is a pending redirect and trigger it.
      */
     private async guard(): Promise<true | UrlTree> {
-        const redirect = CoreApp.getRedirect();
+        const redirect = CoreApp.consumeRedirect();
 
         if (!redirect) {
             return true;
         }
 
-        try {
-            // Only accept the redirect if it was stored less than 20 seconds ago.
-            if (!redirect.timemodified || Date.now() - redirect.timemodified < 20000) {
-                return true;
-            }
+        // Only accept the redirect if it was stored less than 20 seconds ago.
+        if (!redirect.timemodified || Date.now() - redirect.timemodified > 20000) {
+            return true;
+        }
 
-            // Redirect to site path.
-            if (redirect.siteId && redirect.siteId !== CoreConstants.NO_SITE_ID) {
-                const loggedIn = await CoreSites.loadSite(
-                    redirect.siteId,
-                    redirect.page,
-                    redirect.params,
-                );
-                const route = Router.parseUrl('/main');
-
-                route.queryParams = {
-                    redirectPath: redirect.page,
-                    redirectParams: redirect.params,
-                };
-
-                return loggedIn ? route : true;
-            }
-
-            // Abort redirect.
-            if (!redirect.page) {
-                return true;
-            }
-
-            // Redirect to non-site path.
-            const route = Router.parseUrl(redirect.page);
+        // Redirect to site path.
+        if (redirect.siteId && redirect.siteId !== CoreConstants.NO_SITE_ID) {
+            const loggedIn = await CoreSites.loadSite(
+                redirect.siteId,
+                redirect.page,
+                redirect.params,
+            );
+            const route = Router.parseUrl('/main/home');
 
             route.queryParams = {
                 redirectPath: redirect.page,
                 redirectParams: redirect.params,
             };
 
-            return route;
-        } finally {
-            CoreApp.forgetRedirect();
+            return loggedIn ? route : true;
         }
+
+        // Abort redirect.
+        if (!redirect.page) {
+            return true;
+        }
+
+        // Redirect to non-site path.
+        const route = Router.parseUrl(redirect.page);
+
+        route.queryParams = {
+            redirectPath: redirect.page,
+            redirectParams: redirect.params,
+        };
+
+        return route;
     }
 
 }
diff --git a/src/core/services/app.ts b/src/core/services/app.ts
index 3859c2236..7a5365563 100644
--- a/src/core/services/app.ts
+++ b/src/core/services/app.ts
@@ -549,6 +549,19 @@ export class CoreAppProvider {
         }
     }
 
+    /**
+     * Retrieve and forget redirect data.
+     *
+     * @return Redirect data if any.
+     */
+    consumeRedirect(): CoreRedirectData | null {
+        const redirect = this.getRedirect();
+
+        this.forgetRedirect();
+
+        return redirect;
+    }
+
     /**
      * Forget redirect data.
      */
@@ -559,7 +572,7 @@ export class CoreAppProvider {
     /**
      * Retrieve redirect data.
      *
-     * @return Object with siteid, state, params and timemodified.
+     * @return Redirect data if any.
      */
     getRedirect(): CoreRedirectData | null {
         return this.redirect || null;
diff --git a/src/core/services/navigator.ts b/src/core/services/navigator.ts
index ab361fcfa..e1c23827f 100644
--- a/src/core/services/navigator.ts
+++ b/src/core/services/navigator.ts
@@ -30,6 +30,7 @@ import { makeSingleton, NavController, Router } from '@singletons';
 import { CoreScreen } from './screen';
 import { filter } from 'rxjs/operators';
 import { CoreApp } from './app';
+import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
 
 const DEFAULT_MAIN_MENU_TAB = CoreMainMenuHomeHandlerService.PAGE_NAME;
 
@@ -208,7 +209,14 @@ export class CoreNavigatorService {
 
         // If we are logged into a different site, log out first.
         if (CoreSites.isLoggedIn() && CoreSites.getCurrentSiteId() !== siteId) {
-            // @todo: Check site plugins and store redirect.
+            if (CoreSitePlugins.hasSitePluginsLoaded) {
+                // The site has site plugins so the app will be restarted. Store the data and logout.
+                CoreApp.instance.storeRedirect(siteId, path, options.params || {});
+
+                await CoreSites.logout();
+
+                return true;
+            }
 
             await CoreSites.logout();
         }