Merge pull request #3560 from alfonso-salces/MOBILE-4245

[4.2] Mobile 4245 - Add new config.json setting to allow to specify staging sites for testing purposes
main
Dani Palou 2023-03-27 08:10:25 +02:00 committed by GitHub
commit 874f47ebad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 248 additions and 106 deletions

View File

@ -85,8 +85,7 @@
"high": 120 "high": 120
}, },
"customurlscheme": "moodlemobile", "customurlscheme": "moodlemobile",
"siteurl": "", "sites": [],
"sitename": "",
"multisitesdisplay": "", "multisitesdisplay": "",
"sitefindersettings": {}, "sitefindersettings": {},
"onlyallowlistedsites": false, "onlyallowlistedsites": false,

View File

@ -261,7 +261,8 @@ export class AddonStorageManagerCoursesStoragePage implements OnInit, OnDestroy
event.stopPropagation(); event.stopPropagation();
try { try {
const siteName = CoreSites.getRequiredCurrentSite().getSiteName(); const site = CoreSites.getRequiredCurrentSite();
const siteName = await site.getSiteName();
this.spaceUsage = await CoreSettingsHelper.deleteSiteStorage(siteName, this.siteId); this.spaceUsage = await CoreSettingsHelper.deleteSiteStorage(siteName, this.siteId);
} catch { } catch {

View File

@ -61,6 +61,7 @@ import { finalize, map, mergeMap } from 'rxjs/operators';
import { firstValueFrom } from '../utils/rxjs'; import { firstValueFrom } from '../utils/rxjs';
import { CoreSiteError } from '@classes/errors/siteerror'; import { CoreSiteError } from '@classes/errors/siteerror';
import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/support/authenticated-support-config'; import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/support/authenticated-support-config';
import { CoreLoginHelper } from '@features/login/services/login-helper';
/** /**
* QR Code type enumeration. * QR Code type enumeration.
@ -289,13 +290,21 @@ export class CoreSite {
* *
* @returns Site name. * @returns Site name.
*/ */
getSiteName(): string { async getSiteName(): Promise<string> {
if (CoreConstants.CONFIG.sitename) { if (this.infos?.sitename) {
// Overridden by config. return this.infos?.sitename;
return CoreConstants.CONFIG.sitename;
} else {
return this.infos?.sitename || '';
} }
// Fallback.
const isSigleFixedSite = await CoreLoginHelper.isSingleFixedSite();
if (isSigleFixedSite) {
const sites = await CoreLoginHelper.getAvailableSites();
return sites[0].name;
}
return '';
} }
/** /**

View File

@ -65,9 +65,14 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
*/ */
async ngAfterViewInit(): Promise<void> { async ngAfterViewInit(): Promise<void> {
if (this.ionInput) { if (this.ionInput) {
// It's an ion-input, use it to get the native element. try {
this.input = await this.ionInput.getInputElement(); // It's an ion-input, use it to get the native element.
this.setData(this.input); this.input = await this.ionInput.getInputElement();
this.setData(this.input);
} catch (error) {
// This should never fail, but it does in some testing environment because Ionic elements are not
// rendered properly. So in case this fails, we'll just ignore the error.
}
return; return;
} }

View File

@ -61,9 +61,9 @@ export class CoreCoursesMyPage implements OnInit, OnDestroy, AsyncDirective {
constructor(protected loadsManager: PageLoadsManager) { constructor(protected loadsManager: PageLoadsManager) {
// Refresh the enabled flags if site is updated. // Refresh the enabled flags if site is updated.
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, async () => {
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite(); this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
this.loadSiteName(); await this.loadSiteName();
}, CoreSites.getCurrentSiteId()); }, CoreSites.getCurrentSiteId());
@ -78,13 +78,13 @@ export class CoreCoursesMyPage implements OnInit, OnDestroy, AsyncDirective {
/** /**
* @inheritdoc * @inheritdoc
*/ */
ngOnInit(): void { async ngOnInit(): Promise<void> {
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite(); this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
const deepLinkManager = new CoreMainMenuDeepLinkManager(); const deepLinkManager = new CoreMainMenuDeepLinkManager();
deepLinkManager.treatLink(); deepLinkManager.treatLink();
this.loadSiteName(); await this.loadSiteName();
this.loadContent(true); this.loadContent(true);
} }
@ -143,8 +143,9 @@ export class CoreCoursesMyPage implements OnInit, OnDestroy, AsyncDirective {
/** /**
* Load the site name. * Load the site name.
*/ */
protected loadSiteName(): void { protected async loadSiteName(): Promise<void> {
this.siteName = CoreSites.getRequiredCurrentSite().getSiteName() || ''; const site = CoreSites.getRequiredCurrentSite();
this.siteName = await site.getSiteName() || '';
} }
/** /**

View File

@ -50,7 +50,7 @@ export class CoreLoginHasSitesGuard implements CanActivate, CanLoad {
return true; return true;
} }
const [path, params] = CoreLoginHelper.getAddSiteRouteInfo(); const [path, params] = await CoreLoginHelper.getAddSiteRouteInfo();
const route = Router.parseUrl(path); const route = Router.parseUrl(path);
route.queryParams = params; route.queryParams = params;

View File

@ -56,7 +56,6 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
identityProviders?: CoreSiteIdentityProvider[]; identityProviders?: CoreSiteIdentityProvider[];
pageLoaded = false; pageLoaded = false;
isBrowserSSO = false; isBrowserSSO = false;
isFixedUrlSet = false;
showForgottenPassword = true; showForgottenPassword = true;
showScanQR = false; showScanQR = false;
loginAttempts = 0; loginAttempts = 0;
@ -99,9 +98,10 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
if (this.siteConfig) { if (this.siteConfig) {
this.treatSiteConfig(); this.treatSiteConfig();
} }
this.isFixedUrlSet = CoreLoginHelper.isFixedUrlSet();
if (this.isFixedUrlSet || !this.siteConfig) { const isSingleFixedSite = await CoreLoginHelper.isSingleFixedSite();
if (isSingleFixedSite || !this.siteConfig) {
// Fixed URL or not siteConfig retrieved from params, we need to check if it uses browser SSO login. // Fixed URL or not siteConfig retrieved from params, we need to check if it uses browser SSO login.
this.checkSite(this.siteUrl, true); this.checkSite(this.siteUrl, true);
} else { } else {
@ -198,12 +198,12 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
/** /**
* Treat the site configuration (if it exists). * Treat the site configuration (if it exists).
*/ */
protected treatSiteConfig(): void { protected async treatSiteConfig(): Promise<void> {
if (this.siteConfig) { if (this.siteConfig) {
this.siteName = CoreConstants.CONFIG.sitename ? CoreConstants.CONFIG.sitename : this.siteConfig.sitename; this.siteName = this.siteConfig.sitename;
this.logoUrl = CoreLoginHelper.getLogoUrl(this.siteConfig); this.logoUrl = CoreLoginHelper.getLogoUrl(this.siteConfig);
this.authInstructions = this.siteConfig.authinstructions || Translate.instant('core.login.loginsteps'); this.authInstructions = this.siteConfig.authinstructions || Translate.instant('core.login.loginsteps');
this.showScanQR = CoreLoginHelper.displayQRInCredentialsScreen(this.siteConfig.tool_mobile_qrcodetype); this.showScanQR = await CoreLoginHelper.displayQRInCredentialsScreen(this.siteConfig.tool_mobile_qrcodetype);
const disabledFeatures = CoreLoginHelper.getDisabledFeatures(this.siteConfig); const disabledFeatures = CoreLoginHelper.getDisabledFeatures(this.siteConfig);
this.identityProviders = CoreLoginHelper.getValidIdentityProviders(this.siteConfig, disabledFeatures); this.identityProviders = CoreLoginHelper.getValidIdentityProviders(this.siteConfig, disabledFeatures);

View File

@ -20,7 +20,6 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreCountry, CoreUtils } from '@services/utils/utils'; import { CoreCountry, CoreUtils } from '@services/utils/utils';
import { CoreWS, CoreWSExternalWarning } from '@services/ws'; import { CoreWS, CoreWSExternalWarning } from '@services/ws';
import { CoreConstants } from '@/core/constants';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreSitePublicConfigResponse } from '@classes/site'; import { CoreSitePublicConfigResponse } from '@classes/site';
import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profile-field-delegate'; import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profile-field-delegate';
@ -235,7 +234,7 @@ export class CoreLoginEmailSignupPage implements OnInit {
*/ */
protected treatSiteConfig(): boolean { protected treatSiteConfig(): boolean {
if (this.siteConfig?.registerauth == 'email' && !CoreLoginHelper.isEmailSignupDisabled(this.siteConfig)) { if (this.siteConfig?.registerauth == 'email' && !CoreLoginHelper.isEmailSignupDisabled(this.siteConfig)) {
this.siteName = CoreConstants.CONFIG.sitename ? CoreConstants.CONFIG.sitename : this.siteConfig.sitename; this.siteName = this.siteConfig.sitename;
this.authInstructions = this.siteConfig.authinstructions; this.authInstructions = this.siteConfig.authinstructions;
this.ageDigitalConsentVerification = this.siteConfig.agedigitalconsentverification; this.ageDigitalConsentVerification = this.siteConfig.agedigitalconsentverification;
this.supportName = this.siteConfig.supportname; this.supportName = this.siteConfig.supportname;

View File

@ -18,12 +18,12 @@
<ion-content class="ion-padding" (keydown)="keyDown($event)" (keyup)="keyUp($event)"> <ion-content class="ion-padding" (keydown)="keyDown($event)" (keyup)="keyUp($event)">
<core-loading [hideUntil]="!showLoading"> <core-loading [hideUntil]="!showLoading">
<div class="list-item-limited-width"> <div class="list-item-limited-width">
<div class="ion-text-wrap ion-text-center ion-margin-bottom" [ngClass]="{'item-avatar-center': showSiteAvatar}"> <div class="ion-text-wrap ion-text-center ion-margin-bottom" [ngClass]="{'item-avatar-center': showUserAvatar}">
<!-- Show user avatar. --> <!-- Show user avatar. -->
<img *ngIf="showSiteAvatar" [src]="userAvatar" class="large-avatar" core-external-content [siteId]="siteId" <img *ngIf="showUserAvatar" [src]="userAvatar" class="large-avatar" core-external-content [siteId]="siteId"
alt="{{ 'core.pictureof' | translate:{$a: userFullName} }}" onError="this.src='assets/img/user-avatar.png'"> alt="{{ 'core.pictureof' | translate:{$a: userFullName} }}" onError="this.src='assets/img/user-avatar.png'">
<div class="core-login-site-logo" *ngIf="!showSiteAvatar"> <div class="core-login-site-logo" *ngIf="!showUserAvatar">
<!-- Show site logo or a default image. --> <!-- Show site logo or a default image. -->
<img *ngIf="logoUrl" [src]="logoUrl" role="presentation" onError="this.src='assets/img/login_logo.png'" alt=""> <img *ngIf="logoUrl" [src]="logoUrl" role="presentation" onError="this.src='assets/img/login_logo.png'" alt="">
<img *ngIf="!logoUrl" src="assets/img/login_logo.png" role="presentation" alt=""> <img *ngIf="!logoUrl" src="assets/img/login_logo.png" role="presentation" alt="">

View File

@ -53,7 +53,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
logoUrl?: string; logoUrl?: string;
identityProviders?: CoreSiteIdentityProvider[]; identityProviders?: CoreSiteIdentityProvider[];
showForgottenPassword = true; showForgottenPassword = true;
showSiteAvatar = false; showUserAvatar = false;
isBrowserSSO = false; isBrowserSSO = false;
isOAuth = false; isOAuth = false;
isLoggedOut: boolean; isLoggedOut: boolean;
@ -108,14 +108,16 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
this.userFullName = site.infos.fullname; this.userFullName = site.infos.fullname;
this.userAvatar = site.infos.userpictureurl; this.userAvatar = site.infos.userpictureurl;
this.siteUrl = site.infos.siteurl; this.siteUrl = site.infos.siteurl;
this.siteName = site.getSiteName(); this.siteName = await site.getSiteName();
this.supportConfig = new CoreUserAuthenticatedSupportConfig(site); this.supportConfig = new CoreUserAuthenticatedSupportConfig(site);
// If login was OAuth we should only reach this page if the OAuth method ID has changed. // If login was OAuth we should only reach this page if the OAuth method ID has changed.
this.isOAuth = site.isOAuth(); this.isOAuth = site.isOAuth();
const sites = await CoreLoginHelper.getAvailableSites();
// Show logo instead of avatar if it's a fixed site. // Show logo instead of avatar if it's a fixed site.
this.showSiteAvatar = !!this.userAvatar && !CoreLoginHelper.getFixedSites(); this.showUserAvatar = !!this.userAvatar && !sites.length;
this.checkSiteConfig(site); this.checkSiteConfig(site);
@ -180,14 +182,18 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
} }
this.isBrowserSSO = !this.isOAuth && CoreLoginHelper.isSSOLoginNeeded(this.siteConfig.typeoflogin); this.isBrowserSSO = !this.isOAuth && CoreLoginHelper.isSSOLoginNeeded(this.siteConfig.typeoflogin);
this.showScanQR = CoreLoginHelper.displayQRInSiteScreen() ||
CoreLoginHelper.displayQRInCredentialsScreen(this.siteConfig.tool_mobile_qrcodetype); this.showScanQR = CoreLoginHelper.displayQRInSiteScreen();
if (!this.showScanQR) {
this.showScanQR = await CoreLoginHelper.displayQRInCredentialsScreen(this.siteConfig.tool_mobile_qrcodetype);
}
await CoreSites.checkApplication(this.siteConfig); await CoreSites.checkApplication(this.siteConfig);
// Check logoURL if user avatar is not set. // Check logoURL if user avatar is not set.
if (this.userAvatar?.startsWith(this.siteUrl + '/theme/image.php')) { if (this.userAvatar?.startsWith(this.siteUrl + '/theme/image.php')) {
this.showSiteAvatar = false; this.showUserAvatar = false;
} }
this.logoUrl = CoreLoginHelper.getLogoUrl(this.siteConfig); this.logoUrl = CoreLoginHelper.getLogoUrl(this.siteConfig);
} }

View File

@ -20,7 +20,7 @@
<div class="ion-text-center ion-padding ion-margin-bottom core-login-site-logo" [class.hidden]="hasSites || enteredSiteUrl"> <div class="ion-text-center ion-padding ion-margin-bottom core-login-site-logo" [class.hidden]="hasSites || enteredSiteUrl">
<img src="assets/img/login_logo.png" class="avatar-full login-logo" role="presentation" alt=""> <img src="assets/img/login_logo.png" class="avatar-full login-logo" role="presentation" alt="">
</div> </div>
<form [formGroup]="siteForm" (ngSubmit)="connect($event, siteForm.value.siteUrl)" *ngIf="!fixedSites" #siteFormEl> <form [formGroup]="siteForm" (ngSubmit)="connect($event, siteForm.value.siteUrl)" *ngIf="!fixedSites && siteForm" #siteFormEl>
<!-- Form to input the site URL if there are no fixed sites. --> <!-- Form to input the site URL if there are no fixed sites. -->
<ng-container *ngIf=" siteSelector=='url'"> <ng-container *ngIf=" siteSelector=='url'">
<ion-item> <ion-item>
@ -101,7 +101,8 @@
</ng-container> </ng-container>
<ng-container *ngIf="showScanQR && !hasSites && !enteredSiteUrl"> <ng-container *ngIf="showScanQR && !hasSites && !enteredSiteUrl">
<div class="ion-text-center ion-padding ion-margin-top core-login-site-qrcode-separator">{{ 'core.login.or' | translate }}</div> <div class="ion-text-center ion-padding ion-margin-top core-login-site-qrcode-separator">{{ 'core.login.or' | translate }}
</div>
<ion-button expand="block" fill="outline" class="ion-margin core-login-site-qrcode" (click)="showInstructionsAndScanQR()" <ion-button expand="block" fill="outline" class="ion-margin core-login-site-qrcode" (click)="showInstructionsAndScanQR()"
aria-haspopup="dialog"> aria-haspopup="dialog">
<ion-icon slot="start" name="fas-qrcode" aria-hidden="true"></ion-icon> <ion-icon slot="start" name="fas-qrcode" aria-hidden="true"></ion-icon>
@ -119,7 +120,7 @@
<!-- Template site selector. --> <!-- Template site selector. -->
<ng-template #sitelisting let-site="site"> <ng-template #sitelisting let-site="site">
<ion-item button (click)="connect($event, site.url, site)" [attr.aria-label]="site.name" detail="true"> <ion-item button (click)="connect($event, site.url, site)" [ngClass]="site.className" [attr.aria-label]="site.name" detail="true">
<ion-thumbnail *ngIf="siteFinderSettings.displayimage" slot="start"> <ion-thumbnail *ngIf="siteFinderSettings.displayimage" slot="start">
<img [src]="site.imageurl" *ngIf="site.imageurl" onError="this.src='assets/icon/icon.png'" alt="" role="presentation"> <img [src]="site.imageurl" *ngIf="site.imageurl" onError="this.src='assets/icon/icon.png'" alt="" role="presentation">
<img src="assets/icon/icon.png" *ngIf="!site.imageurl" class="core-login-default-icon" alt="" role="presentation"> <img src="assets/icon/icon.png" *ngIf="!site.imageurl" class="core-login-default-icon" alt="" role="presentation">

View File

@ -59,7 +59,7 @@ export class CoreLoginSitePage implements OnInit {
@ViewChild('siteFormEl') formElement?: ElementRef; @ViewChild('siteFormEl') formElement?: ElementRef;
siteForm: FormGroup; siteForm!: FormGroup;
fixedSites?: CoreLoginSiteInfoExtended[]; fixedSites?: CoreLoginSiteInfoExtended[];
filteredSites?: CoreLoginSiteInfoExtended[]; filteredSites?: CoreLoginSiteInfoExtended[];
siteSelector: CoreLoginSiteSelectorListMethod = 'sitefinder'; siteSelector: CoreLoginSiteSelectorListMethod = 'sitefinder';
@ -68,15 +68,17 @@ export class CoreLoginSitePage implements OnInit {
sites: CoreLoginSiteInfoExtended[] = []; sites: CoreLoginSiteInfoExtended[] = [];
hasSites = false; hasSites = false;
loadingSites = false; loadingSites = false;
searchFunction: (search: string) => void; searchFunction!: (search: string) => void;
showScanQR: boolean; showScanQR!: boolean;
enteredSiteUrl?: CoreLoginSiteInfoExtended; enteredSiteUrl?: CoreLoginSiteInfoExtended;
siteFinderSettings: CoreLoginSiteFinderSettings; siteFinderSettings!: CoreLoginSiteFinderSettings;
constructor( constructor(protected formBuilder: FormBuilder) {}
protected formBuilder: FormBuilder,
) {
/**
* Initialize the component.
*/
async ngOnInit(): Promise<void> {
let url = ''; let url = '';
this.siteSelector = CoreConstants.CONFIG.multisitesdisplay; this.siteSelector = CoreConstants.CONFIG.multisitesdisplay;
@ -92,8 +94,10 @@ export class CoreLoginSitePage implements OnInit {
}; };
// Load fixed sites if they're set. // Load fixed sites if they're set.
if (CoreLoginHelper.hasSeveralFixedSites()) { const sites = await CoreLoginHelper.getAvailableSites();
url = this.initSiteSelector();
if (sites.length) {
url = await this.initSiteSelector();
} else if (CoreConstants.CONFIG.enableonboarding && !CorePlatform.isIOS()) { } else if (CoreConstants.CONFIG.enableonboarding && !CorePlatform.isIOS()) {
this.initOnboarding(); this.initOnboarding();
} }
@ -122,12 +126,7 @@ export class CoreLoginSitePage implements OnInit {
this.loadingSites = false; this.loadingSites = false;
}, 1000); }, 1000);
}
/**
* Initialize the component.
*/
ngOnInit(): void {
this.showKeyboard = !!CoreNavigator.getRouteBooleanParam('showKeyboard'); this.showKeyboard = !!CoreNavigator.getRouteBooleanParam('showKeyboard');
} }
@ -136,8 +135,9 @@ export class CoreLoginSitePage implements OnInit {
* *
* @returns URL of the first site. * @returns URL of the first site.
*/ */
protected initSiteSelector(): string { protected async initSiteSelector(): Promise<string> {
this.fixedSites = this.extendCoreLoginSiteInfo(<CoreLoginSiteInfoExtended[]> CoreLoginHelper.getFixedSites()); const availableSites = await CoreLoginHelper.getAvailableSites();
this.fixedSites = this.extendCoreLoginSiteInfo(<CoreLoginSiteInfoExtended[]> availableSites);
this.siteSelector = 'list'; // In case it's not defined this.siteSelector = 'list'; // In case it's not defined
// Do not show images if none are set. // Do not show images if none are set.

View File

@ -40,6 +40,7 @@ import { CorePath } from '@singletons/path';
import { CorePromisedValue } from '@classes/promised-value'; import { CorePromisedValue } from '@classes/promised-value';
import { SafeHtml } from '@angular/platform-browser'; import { SafeHtml } from '@angular/platform-browser';
import { CoreLoginError } from '@classes/errors/loginerror'; import { CoreLoginError } from '@classes/errors/loginerror';
import { CoreSettingsHelper } from '@features/settings/services/settings-helper';
const PASSWORD_RESETS_CONFIG_KEY = 'password-resets'; const PASSWORD_RESETS_CONFIG_KEY = 'password-resets';
@ -379,9 +380,23 @@ export class CoreLoginHelperProvider {
* Get fixed site or sites. * Get fixed site or sites.
* *
* @returns Fixed site or list of fixed sites. * @returns Fixed site or list of fixed sites.
* @deprecated since 4.2.0. Use CoreConstants.CONFIG.sites or getAvailableSites() instead.
*/ */
getFixedSites(): string | CoreLoginSiteInfo[] { getFixedSites(): string | CoreLoginSiteInfo[] {
return CoreConstants.CONFIG.siteurl; const notStagingSites = CoreConstants.CONFIG.sites.filter(site => !site.staging);
return notStagingSites.length === 1 ? notStagingSites[0].url : notStagingSites;
}
/**
* Get Available sites (includes staging sites if are enabled).
*
* @returns Available sites.
*/
async getAvailableSites(): Promise<CoreLoginSiteInfo[]> {
const hasEnabledStagingSites = await CoreSettingsHelper.hasEnabledStagingSites();
return hasEnabledStagingSites ? CoreConstants.CONFIG.sites : CoreConstants.CONFIG.sites.filter(site => !site.staging);
} }
/** /**
@ -440,7 +455,7 @@ export class CoreLoginHelperProvider {
return; return;
} }
} else { } else {
[path, params] = this.getAddSiteRouteInfo(showKeyboard); [path, params] = await this.getAddSiteRouteInfo(showKeyboard);
} }
await CoreNavigator.navigate(path, { params, reset: setRoot }); await CoreNavigator.navigate(path, { params, reset: setRoot });
@ -452,13 +467,12 @@ export class CoreLoginHelperProvider {
* @param showKeyboard Whether to show keyboard in the new page. Only if no fixed URL set. * @param showKeyboard Whether to show keyboard in the new page. Only if no fixed URL set.
* @returns Path and params. * @returns Path and params.
*/ */
getAddSiteRouteInfo(showKeyboard?: boolean): [string, Params] { async getAddSiteRouteInfo(showKeyboard?: boolean): Promise<[string, Params]> {
if (this.isFixedUrlSet()) { const sites = await this.getAvailableSites();
// Fixed URL is set, go to credentials page.
const fixedSites = this.getFixedSites();
const url = typeof fixedSites == 'string' ? fixedSites : fixedSites[0].url;
return ['/login/credentials', { siteUrl: url }]; if (sites.length === 1) {
// Fixed URL is set, go to credentials page.
return ['/login/credentials', { siteUrl: sites[0].url }];
} }
return ['/login/site', { showKeyboard }]; return ['/login/site', { showKeyboard }];
@ -518,10 +532,12 @@ export class CoreLoginHelperProvider {
* Check if the app is configured to use several fixed URLs. * Check if the app is configured to use several fixed URLs.
* *
* @returns Whether there are several fixed URLs. * @returns Whether there are several fixed URLs.
* @deprecated 4.2.0 Use CoreConstants.CONFIG.sites.length > 1 instead.
*/ */
hasSeveralFixedSites(): boolean { async hasSeveralFixedSites(): Promise<boolean> {
return !!(CoreConstants.CONFIG.siteurl && Array.isArray(CoreConstants.CONFIG.siteurl) && const sites = await this.getAvailableSites();
CoreConstants.CONFIG.siteurl.length > 1);
return sites.length > 1;
} }
/** /**
@ -557,13 +573,21 @@ export class CoreLoginHelperProvider {
* Check if the app is configured to use a fixed URL (only 1). * Check if the app is configured to use a fixed URL (only 1).
* *
* @returns Whether there is 1 fixed URL. * @returns Whether there is 1 fixed URL.
* @deprecated 4.2.0 Use isSingleFixedSite instead.
*/ */
isFixedUrlSet(): boolean { isFixedUrlSet(): boolean {
if (Array.isArray(CoreConstants.CONFIG.siteurl)) { return CoreConstants.CONFIG.sites.filter(site => !site.staging).length === 1;
return CoreConstants.CONFIG.siteurl.length == 1; }
}
return !!CoreConstants.CONFIG.siteurl; /**
* Check if the app is configured to use a fixed URL (only 1).
*
* @returns Whether there is 1 fixed URL.
*/
async isSingleFixedSite(): Promise<boolean> {
const sites = await this.getAvailableSites();
return sites.length === 1;
} }
/** /**
@ -606,12 +630,9 @@ export class CoreLoginHelperProvider {
* @returns Promise resolved with boolean: whether is one of the fixed sites. * @returns Promise resolved with boolean: whether is one of the fixed sites.
*/ */
async isSiteUrlAllowed(siteUrl: string, checkSiteFinder = true): Promise<boolean> { async isSiteUrlAllowed(siteUrl: string, checkSiteFinder = true): Promise<boolean> {
if (this.isFixedUrlSet()) { const sites = await this.getAvailableSites();
// Only 1 site allowed.
return CoreUrl.sameDomainAndPath(siteUrl, <string> this.getFixedSites());
} else if (this.hasSeveralFixedSites()) {
const sites = <CoreLoginSiteInfo[]> this.getFixedSites();
if (sites.length) {
return sites.some((site) => CoreUrl.sameDomainAndPath(siteUrl, site.url)); return sites.some((site) => CoreUrl.sameDomainAndPath(siteUrl, site.url));
} else if (CoreConstants.CONFIG.multisitesdisplay == 'sitefinder' && CoreConstants.CONFIG.onlyallowlistedsites && } else if (CoreConstants.CONFIG.multisitesdisplay == 'sitefinder' && CoreConstants.CONFIG.onlyallowlistedsites &&
checkSiteFinder) { checkSiteFinder) {
@ -1303,12 +1324,14 @@ export class CoreLoginHelperProvider {
* @param qrCodeType QR Code type from public config, assuming enabled if undefined. * @param qrCodeType QR Code type from public config, assuming enabled if undefined.
* @returns Whether the QR reader should be displayed in credentials screen. * @returns Whether the QR reader should be displayed in credentials screen.
*/ */
displayQRInCredentialsScreen(qrCodeType = CoreSiteQRCodeType.QR_CODE_LOGIN): boolean { async displayQRInCredentialsScreen(qrCodeType = CoreSiteQRCodeType.QR_CODE_LOGIN): Promise<boolean> {
if (!CoreUtils.canScanQR()) { if (!CoreUtils.canScanQR()) {
return false; return false;
} }
if ((CoreConstants.CONFIG.displayqroncredentialscreen === undefined && this.isFixedUrlSet()) || const isSingleFixedSite = await this.isSingleFixedSite();
if ((CoreConstants.CONFIG.displayqroncredentialscreen === undefined && isSingleFixedSite) ||
(CoreConstants.CONFIG.displayqroncredentialscreen !== undefined && (CoreConstants.CONFIG.displayqroncredentialscreen !== undefined &&
!!CoreConstants.CONFIG.displayqroncredentialscreen)) { !!CoreConstants.CONFIG.displayqroncredentialscreen)) {

View File

@ -18,17 +18,26 @@ import { CoreLoginError } from '@classes/errors/loginerror';
import { CoreLoginComponentsModule } from '@features/login/components/components.module'; import { CoreLoginComponentsModule } from '@features/login/components/components.module';
import { CoreLoginCredentialsPage } from '@features/login/pages/credentials/credentials'; import { CoreLoginCredentialsPage } from '@features/login/pages/credentials/credentials';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { Http } from '@singletons';
import { of } from 'rxjs';
import { CoreLoginHelper } from '../services/login-helper';
describe('Credentials page', () => { describe('Credentials page', () => {
const siteUrl = 'https://campus.example.edu';
beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mockSingleton(Http, { get: () => of(null as any) });
});
it('renders', async () => { it('renders', async () => {
// Arrange. // Arrange.
const siteUrl = 'https://campus.example.edu';
mockSingleton(CoreSites, { mockSingleton(CoreSites, {
getPublicSiteConfigByUrl: async () => ({ getPublicSiteConfigByUrl: async () => ({
wwwroot: 'https://campus.example.edu', wwwroot: siteUrl,
httpswwwroot: 'https://campus.example.edu', httpswwwroot: siteUrl,
sitename: 'Example Campus', sitename: 'Example Campus',
guestlogin: 0, guestlogin: 0,
rememberusername: 0, rememberusername: 0,
@ -45,6 +54,8 @@ describe('Credentials page', () => {
}), }),
}); });
mockSingleton(CoreLoginHelper, { getAvailableSites: async () => [{ url: siteUrl, name: 'Example Campus' }] });
// Act. // Act.
const fixture = await renderPageComponent(CoreLoginCredentialsPage, { const fixture = await renderPageComponent(CoreLoginCredentialsPage, {
routeParams: { siteUrl }, routeParams: { siteUrl },
@ -58,7 +69,7 @@ describe('Credentials page', () => {
expect(findElement(fixture, '.core-siteurl', siteUrl)).not.toBeNull(); expect(findElement(fixture, '.core-siteurl', siteUrl)).not.toBeNull();
}); });
it('suggests contacting support after multiple failed attempts', async () => { it('suggests contacting support after multiple failed attempts', async (done) => {
// Arrange. // Arrange.
mockSingleton(CoreSites, { mockSingleton(CoreSites, {
getUserToken: () => { getUserToken: () => {
@ -69,17 +80,15 @@ describe('Credentials page', () => {
}, },
}); });
mockSingleton(CoreLoginHelper, { getAvailableSites: async () => [] });
const fixture = await renderPageComponent(CoreLoginCredentialsPage, { const fixture = await renderPageComponent(CoreLoginCredentialsPage, {
routeParams: { routeParams: { siteUrl, siteConfig: { supportpage: '' } },
siteUrl: 'https://campus.example.edu', imports: [CoreSharedModule, CoreLoginComponentsModule],
siteConfig: { supportpage: '' },
},
imports: [
CoreSharedModule,
CoreLoginComponentsModule,
],
}); });
done();
// Act. // Act.
const form = requireElement<HTMLFormElement>(fixture, 'form'); const form = requireElement<HTMLFormElement>(fixture, 'form');
const formControls = fixture.componentInstance.credForm.controls; const formControls = fixture.componentInstance.credForm.controls;

View File

@ -65,7 +65,7 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
const currentSite = CoreSites.getRequiredCurrentSite(); const currentSite = CoreSites.getRequiredCurrentSite();
this.siteId = currentSite.getId(); this.siteId = currentSite.getId();
this.siteInfo = currentSite.getInfo(); this.siteInfo = currentSite.getInfo();
this.siteName = currentSite.getSiteName(); this.siteName = await currentSite.getSiteName();
this.siteUrl = currentSite.getURL(); this.siteUrl = currentSite.getURL();
this.displaySwitchAccount = !currentSite.isFeatureDisabled('NoDelegate_SwitchAccount'); this.displaySwitchAccount = !currentSite.isFeatureDisabled('NoDelegate_SwitchAccount');
this.displayContactSupport = new CoreUserAuthenticatedSupportConfig(currentSite).canContactSupport(); this.displayContactSupport = new CoreUserAuthenticatedSupportConfig(currentSite).canContactSupport();

View File

@ -45,18 +45,18 @@ export class CoreMainMenuHomePage implements OnInit {
/** /**
* @inheritdoc * @inheritdoc
*/ */
ngOnInit(): void { async ngOnInit(): Promise<void> {
this.deepLinkManager = new CoreMainMenuDeepLinkManager(); this.deepLinkManager = new CoreMainMenuDeepLinkManager();
this.loadSiteName(); await this.loadSiteName();
this.subscription = CoreMainMenuHomeDelegate.getHandlersObservable().subscribe((handlers) => { this.subscription = CoreMainMenuHomeDelegate.getHandlersObservable().subscribe((handlers) => {
handlers && this.initHandlers(handlers); handlers && this.initHandlers(handlers);
}); });
// Refresh the enabled flags if site is updated. // Refresh the enabled flags if site is updated.
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, async () => {
this.loadSiteName(); await this.loadSiteName();
}, CoreSites.getCurrentSiteId()); }, CoreSites.getCurrentSiteId());
} }
@ -99,8 +99,9 @@ export class CoreMainMenuHomePage implements OnInit {
/** /**
* Load the site name. * Load the site name.
*/ */
protected loadSiteName(): void { protected async loadSiteName(): Promise<void> {
this.siteName = CoreSites.getRequiredCurrentSite().getSiteName() || ''; const site = CoreSites.getRequiredCurrentSite();
this.siteName = await site.getSiteName() || '';
} }
/** /**

View File

@ -30,6 +30,12 @@
</ion-label> </ion-label>
<ion-toggle [(ngModel)]="forceSafeAreaMargins" (ionChange)="safeAreaChanged()"></ion-toggle> <ion-toggle [(ngModel)]="forceSafeAreaMargins" (ionChange)="safeAreaChanged()"></ion-toggle>
</ion-item> </ion-item>
<ion-item class="ion-text-wrap" *ngIf="stagingSitesCount && enableStagingSites !== undefined">
<ion-label>
<h2>Enable staging sites ({{stagingSitesCount}})</h2>
</ion-label>
<ion-toggle [(ngModel)]="enableStagingSites" (ionChange)="setEnabledStagingSites($event.detail.checked)"></ion-toggle>
</ion-item>
<ng-container *ngIf="siteId"> <ng-container *ngIf="siteId">
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label> <ion-label>

View File

@ -12,8 +12,10 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { CoreConstants } from '@/core/constants';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { CoreLoginHelperProvider } from '@features/login/services/login-helper'; import { CoreLoginHelperProvider } from '@features/login/services/login-helper';
import { CoreSettingsHelper } from '@features/settings/services/settings-helper';
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
import { CoreUserTours } from '@features/usertours/services/user-tours'; import { CoreUserTours } from '@features/usertours/services/user-tours';
import { CoreConfig } from '@services/config'; import { CoreConfig } from '@services/config';
@ -41,6 +43,9 @@ export class CoreSettingsDevPage implements OnInit {
pluginStylesCount = 0; pluginStylesCount = 0;
sitePlugins: CoreSitePluginsBasicInfo[] = []; sitePlugins: CoreSitePluginsBasicInfo[] = [];
userToursEnabled = true; userToursEnabled = true;
stagingSitesCount = 0;
enableStagingSites?: boolean;
listenStagingSitesChanges = false;
disabledFeatures: string[] = []; disabledFeatures: string[] = [];
@ -55,6 +60,13 @@ export class CoreSettingsDevPage implements OnInit {
this.siteId = CoreSites.getCurrentSite()?.getId(); this.siteId = CoreSites.getCurrentSite()?.getId();
this.stagingSitesCount = CoreConstants.CONFIG.sites.filter((site) => site.staging).length;
if (this.stagingSitesCount) {
this.enableStagingSites = await CoreSettingsHelper.hasEnabledStagingSites();
this.listenStagingSitesChanges = true;
}
if (!this.siteId) { if (!this.siteId) {
return; return;
} }
@ -157,6 +169,22 @@ export class CoreSettingsDevPage implements OnInit {
CoreDomUtils.showToast('User tours have been reseted'); CoreDomUtils.showToast('User tours have been reseted');
} }
async setEnabledStagingSites(enabled: boolean): Promise<void> {
if (!this.listenStagingSitesChanges) {
this.listenStagingSitesChanges = true;
return;
}
try {
await CoreSettingsHelper.setEnabledStagingSites(enabled);
} catch (error) {
this.enableStagingSites = !enabled;
this.listenStagingSitesChanges = false;
CoreDomUtils.showErrorModal(error);
}
}
} }
// Basic site plugin info. // Basic site plugin info.

View File

@ -27,6 +27,7 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CorePlatform } from '@services/platform'; import { CorePlatform } from '@services/platform';
import { CoreNetwork } from '@services/network'; import { CoreNetwork } from '@services/network';
import { CoreLoginHelper } from '@features/login/services/login-helper';
/** /**
* Device Info to be shown and copied to clipboard. * Device Info to be shown and copied to clipboard.
@ -167,10 +168,6 @@ export class CoreSettingsDeviceInfoPage implements OnDestroy {
} }
const currentSite = sitesProvider.getCurrentSite(); const currentSite = sitesProvider.getCurrentSite();
this.deviceInfo.siteUrl = (currentSite?.getURL()) ||
(typeof CoreConstants.CONFIG.siteurl == 'string' && CoreConstants.CONFIG.siteurl) || undefined;
this.deviceInfo.isPrefixedUrl = !!CoreConstants.CONFIG.siteurl;
this.deviceInfo.siteId = currentSite?.getId(); this.deviceInfo.siteId = currentSite?.getId();
this.deviceInfo.siteVersion = currentSite?.getInfo()?.release; this.deviceInfo.siteVersion = currentSite?.getInfo()?.release;
@ -189,12 +186,21 @@ export class CoreSettingsDeviceInfoPage implements OnDestroy {
* Async part of the constructor. * Async part of the constructor.
*/ */
protected async asyncInit(): Promise<void> { protected async asyncInit(): Promise<void> {
const sitesProvider = CoreSites.instance;
const fileProvider = CoreFile.instance; const fileProvider = CoreFile.instance;
const lang = await CoreLang.getCurrentLanguage(); const lang = await CoreLang.getCurrentLanguage();
this.deviceInfo.currentLanguage = lang; this.deviceInfo.currentLanguage = lang;
this.currentLangName = CoreConstants.CONFIG.languages[lang]; this.currentLangName = CoreConstants.CONFIG.languages[lang];
const currentSite = sitesProvider.getCurrentSite();
const isSingleFixedSite = await CoreLoginHelper.isSingleFixedSite();
const sites = await CoreLoginHelper.getAvailableSites();
const firstUrl = isSingleFixedSite && sites[0].url;
this.deviceInfo.siteUrl = currentSite?.getURL() || firstUrl || undefined;
this.deviceInfo.isPrefixedUrl = !!sites.length;
if (fileProvider.isAvailable()) { if (fileProvider.isAvailable()) {
const basepath = await fileProvider.getBasePath(); const basepath = await fileProvider.getBasePath();
this.deviceInfo.fileSystemRoot = basepath; this.deviceInfo.fileSystemRoot = basepath;

View File

@ -66,7 +66,7 @@ export class CoreSettingsSpaceUsagePage implements OnInit, OnDestroy {
const site = await CoreSites.getSite(siteId); const site = await CoreSites.getSite(siteId);
const siteInfo = site.getInfo(); const siteInfo = site.getInfo();
siteEntry.siteName = site.getSiteName(); siteEntry.siteName = await site.getSiteName();
if (siteInfo) { if (siteInfo) {
siteEntry.siteUrl = siteInfo.siteurl; siteEntry.siteUrl = siteInfo.siteurl;

View File

@ -80,7 +80,7 @@ export class CoreSettingsSynchronizationPage implements OnInit, OnDestroy {
const siteInfo = site.getInfo(); const siteInfo = site.getInfo();
siteEntry.siteName = site.getSiteName(); siteEntry.siteName = await site.getSiteName();
if (siteInfo) { if (siteInfo) {
siteEntry.siteUrl = siteInfo.siteurl; siteEntry.siteUrl = siteInfo.siteurl;

View File

@ -30,6 +30,7 @@ import { makeSingleton, Translate } from '@singletons';
import { CoreError } from '@classes/errors/error'; import { CoreError } from '@classes/errors/error';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreNavigator } from '@services/navigator';
/** /**
* Object with space usage and cache entries that can be erased. * Object with space usage and cache entries that can be erased.
@ -477,6 +478,39 @@ export class CoreSettingsHelperProvider {
return this.darkModeObservable; return this.darkModeObservable;
} }
/**
* Get if user enabled staging sites or not.
*
* @returns Staging sites.
*/
async hasEnabledStagingSites(): Promise<boolean> {
const staging = await CoreConfig.get<number>('stagingSites', 0);
return !!staging;
}
/**
* Persist staging sites enabled status and refresh app to apply changes.
*
* @param enabled Enabled or disabled staging sites.
*/
async setEnabledStagingSites(enabled: boolean): Promise<void> {
const reloadApp = !CoreSites.isLoggedIn();
if (reloadApp) {
await CoreDomUtils.showConfirm('Are you sure that you want to enable/disable staging sites?');
}
await CoreConfig.set('stagingSites', enabled ? 1 : 0);
if (!reloadApp) {
return;
}
await CoreNavigator.navigate('/');
window.location.reload();
}
} }
export const CoreSettingsHelper = makeSingleton(CoreSettingsHelperProvider); export const CoreSettingsHelper = makeSingleton(CoreSettingsHelperProvider);

View File

@ -15,6 +15,7 @@
import { mock, mockSingleton } from '@/testing/utils'; import { mock, mockSingleton } from '@/testing/utils';
import { CoreSite } from '@classes/site'; import { CoreSite } from '@classes/site';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreSiteHomeIndexLinkHandlerService } from '@features/sitehome/services/handlers/index-link'; import { CoreSiteHomeIndexLinkHandlerService } from '@features/sitehome/services/handlers/index-link';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
@ -33,6 +34,8 @@ describe('Site Home link handlers', () => {
getSiteIdsFromUrl: () => Promise.resolve([siteId]), getSiteIdsFromUrl: () => Promise.resolve([siteId]),
})); }));
mockSingleton(CoreLoginHelper, { getAvailableSites: async () => [{ url: siteUrl, name: 'Example Campus' }] });
CoreContentLinksDelegate.registerHandler(new CoreSiteHomeIndexLinkHandlerService()); CoreContentLinksDelegate.registerHandler(new CoreSiteHomeIndexLinkHandlerService());
// Act. // Act.

View File

@ -1261,7 +1261,7 @@ export class CoreSitesProvider {
siteUrl: site.siteUrl, siteUrl: site.siteUrl,
siteUrlWithoutProtocol: site.siteUrl.replace(/^https?:\/\//, '').toLowerCase(), siteUrlWithoutProtocol: site.siteUrl.replace(/^https?:\/\//, '').toLowerCase(),
fullName: siteInfo?.fullname, fullName: siteInfo?.fullname,
siteName: CoreConstants.CONFIG.sitename == '' ? siteInfo?.sitename: CoreConstants.CONFIG.sitename, siteName: siteInfo?.sitename,
avatar: siteInfo?.userpictureurl, avatar: siteInfo?.userpictureurl,
siteHomeId: siteInfo?.siteid || 1, siteHomeId: siteInfo?.siteid || 1,
loggedOut: !!site.loggedOut, loggedOut: !!site.loggedOut,
@ -2105,6 +2105,16 @@ export type CoreLoginSiteInfo = {
* Countrycode of the site. * Countrycode of the site.
*/ */
countrycode?: string; countrycode?: string;
/**
* Is staging site.
*/
staging?: boolean;
/**
* Class to apply to site item.
*/
className?: string;
}; };
/** /**

View File

@ -109,7 +109,9 @@ export class CoreUpdateManagerProvider {
* @returns Promise resolved when done. * @returns Promise resolved when done.
*/ */
protected async checkCurrentSiteAllowed(): Promise<void> { protected async checkCurrentSiteAllowed(): Promise<void> {
if (!CoreLoginHelper.getFixedSites()) { const sites = await CoreLoginHelper.getAvailableSites();
if (!sites.length) {
return; return;
} }

View File

@ -42,8 +42,7 @@ export interface EnvironmentConfig {
zoomlevels: Record<CoreZoomLevel, number>; zoomlevels: Record<CoreZoomLevel, number>;
defaultZoomLevel?: CoreZoomLevel; // Set the default zoom level of the app. defaultZoomLevel?: CoreZoomLevel; // Set the default zoom level of the app.
customurlscheme: string; customurlscheme: string;
siteurl: string | CoreLoginSiteInfo[]; sites: CoreLoginSiteInfo[];
sitename: string;
multisitesdisplay: CoreLoginSiteSelectorListMethod; multisitesdisplay: CoreLoginSiteSelectorListMethod;
sitefindersettings: Partial<CoreLoginSiteFinderSettings>; sitefindersettings: Partial<CoreLoginSiteFinderSettings>;
onlyallowlistedsites: boolean; onlyallowlistedsites: boolean;