Merge pull request #2582 from dpalou/MOBILE-3565

Mobile 3565
main
Dani Palou 2020-10-30 12:50:22 +01:00 committed by GitHub
commit f519d56ea7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 456 additions and 25 deletions

2
package-lock.json generated
View File

@ -6454,7 +6454,7 @@
"integrity": "sha512-EYC5eQFVkoYXq39l7tYKE6lEjHJ04mvTmKXxGL7quHLdFPfJMNzru/UYpn92AOfpl3PQaZmou78C7EgmFOwFQQ=="
},
"cordova-plugin-wkuserscript": {
"version": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git#6413f4bb3c2565f353e690b5c1450b69ad9e860e",
"version": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git#aa77d0f98a3fb106f2e798e5adf5882f01a2c947",
"from": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git"
},
"cordova-plugin-wkwebview-cookies": {

View File

@ -16,7 +16,8 @@ import { Component, OnInit } from '@angular/core';
import { NavController } from '@ionic/angular';
import { CoreLangProvider } from '@services/lang';
import { CoreEvents } from '@singletons/events';
import { CoreLoginHelperProvider } from '@core/login/services/helper';
import { CoreEvents, CoreEventSessionExpiredData } from '@singletons/events';
@Component({
selector: 'app-root',
@ -28,6 +29,7 @@ export class AppComponent implements OnInit {
constructor(
protected langProvider: CoreLangProvider,
protected navCtrl: NavController,
protected loginHelper: CoreLoginHelperProvider,
) {
}
@ -46,6 +48,11 @@ export class AppComponent implements OnInit {
// @todo
// this.removeVersionClass();
});
// Listen for session expired events.
CoreEvents.on(CoreEvents.SESSION_EXPIRED, (data: CoreEventSessionExpiredData) => {
this.loginHelper.sessionExpired(data);
});
}
}

View File

@ -2,12 +2,12 @@
<ng-container *ngIf="control && control.dirty && !control.valid">
<ng-container *ngFor="let error of errorKeys">
<div *ngIf="control.hasError(error)" class="core-input-error">
<span *ngIf="errorMessages[error]">{{errorMessages[error]}}</span>
<span *ngIf="!errorMessages[error] && error == 'max' && control.errors.max">
{{ 'core.login.invalidvaluemax' | translate:{$a: control.errors.max.max} }}
<span *ngIf="errorMessages && errorMessages[error]">{{errorMessages[error]}}</span>
<span *ngIf="(!errorMessages || !errorMessages[error]) && error == 'max' && control.errors?.max">
{{ 'core.login.invalidvaluemax' | translate:{$a: control.errors!.max.max} }}
</span>
<span *ngIf="!errorMessages[error] && error == 'min' && control.errors.min">
{{ 'core.login.invalidvaluemin' | translate:{$a: control.errors.min.min} }}
<span *ngIf="(!errorMessages || !errorMessages[error]) && error == 'min' && control.errors?.min">
{{ 'core.login.invalidvaluemin' | translate:{$a: control.errors!.min.min} }}
</span>
</div>
</ng-container>

View File

@ -39,21 +39,25 @@ const routes: Routes = [
},
{
path: 'forgottenpassword',
loadChildren: () => import('./pages/forgotten-password/forgotten-password.module')
loadChildren: () => import('./pages/forgotten-password/forgotten-password.page.module')
.then( m => m.CoreLoginForgottenPasswordPageModule),
},
{
path: 'changepassword',
loadChildren: () => import('./pages/change-password/change-password.module')
loadChildren: () => import('./pages/change-password/change-password.page.module')
.then( m => m.CoreLoginChangePasswordPageModule),
},
{
path: 'sitepolicy',
loadChildren: () => import('./pages/site-policy/site-policy.module').then( m => m.CoreLoginSitePolicyPageModule),
loadChildren: () => import('./pages/site-policy/site-policy.page.module').then( m => m.CoreLoginSitePolicyPageModule),
},
{
path: 'emailsignup',
loadChildren: () => import('./pages/email-signup/email-signup.module').then( m => m.CoreLoginEmailSignupPageModule),
loadChildren: () => import('./pages/email-signup/email-signup.page.module').then( m => m.CoreLoginEmailSignupPageModule),
},
{
path: 'reconnect',
loadChildren: () => import('./pages/reconnect/reconnect.page.module').then( m => m.CoreLoginReconnectPageModule),
},
];

View File

@ -13,7 +13,7 @@
}
.core-sitename {
font-size: 1.8rem;
font-size: 1.2rem;
}
.core-login-site-logo {

View File

@ -28,7 +28,7 @@ import { CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@classes
import { CoreEvents } from '@singletons/events';
/**
* Page that displays a "splash screen" while the app is being initialized.
* Page to enter the user credentials.
*/
@Component({
selector: 'page-core-login-credentials',
@ -161,7 +161,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
*/
protected treatSiteConfig(): void {
if (this.siteConfig) {
this.siteName = CoreConstants.CONFIG.sitename ?? this.siteConfig.sitename;
this.siteName = CoreConstants.CONFIG.sitename ? CoreConstants.CONFIG.sitename : this.siteConfig.sitename;
this.logoUrl = CoreLoginHelper.instance.getLogoUrl(this.siteConfig);
this.authInstructions = this.siteConfig.authinstructions || Translate.instance.instant('core.login.loginsteps');

View File

@ -148,7 +148,7 @@
<ion-input type="text" name="nameField" placeholder="{{ 'core.user.' + nameField | translate }}"
formControlName="{{nameField}}" autocorrect="off">
</ion-input>
<core-input-errors [control]="signupForm.controls[nameField]" [errorMessages]="namefieldsErrors[nameField]">
<core-input-errors [control]="signupForm.controls[nameField]" [errorMessages]="namefieldsErrors![nameField]">
</core-input-errors>
</ion-item>
<ion-item class="ion-text-wrap">

View File

@ -337,7 +337,7 @@ export class CoreLoginEmailSignupPage implements OnInit {
/**
* Show authentication instructions.
*/
protected showAuthInstructions(): void {
showAuthInstructions(): void {
CoreTextUtils.instance.viewText(Translate.instance.instant('core.login.instructions'), this.authInstructions!);
}

View File

@ -0,0 +1,90 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>{{ 'core.login.reconnect' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="ion-text-wrap ion-text-center ion-margin-bottom" [ngClass]="{'item-avatar-center': showSiteAvatar}">
<ng-container *ngIf="showSiteAvatar">
<ion-avatar>
<!-- Show user avatar. -->
<img [src]="userAvatar" class="avatar" core-external-content [siteId]="siteId" role="presentation"
alt="{{ 'core.pictureof' | translate:{$a: userFullName} }}" onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
</ng-container>
<div class="core-login-site-logo" *ngIf="!showSiteAvatar">
<!-- Show site logo or a default image. -->
<img *ngIf="logoUrl" [src]="logoUrl" role="presentation" onError="this.src='assets/img/login_logo.png'">
<img *ngIf="!logoUrl" src="assets/img/login_logo.png" role="presentation">
</div>
<h6 *ngIf="siteName" class="ion-padding core-sitename">
<core-format-text [text]="siteName" [filter]="false"></core-format-text>
</h6>
<p class="core-siteurl">{{siteUrl}}</p>
<ion-item *ngIf="!isLoggedOut" class="ion-text-center core-login-reconnect-warning" lines="none">
<ion-label color="danger">
<ion-icon name="fas-exclamation-circle" slot="start"></ion-icon>
{{ 'core.login.reconnectdescription' | translate }}
</ion-label>
</ion-item>
</div>
<form ion-list *ngIf="!isOAuth" [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #reconnectForm>
<ion-item class="ion-text-wrap core-username">
<ion-label>
<p>{{username}}</p>
</ion-label>
</ion-item>
<ion-item class="ion-margin-bottom">
<core-show-password [name]="'password'">
<ion-input class="core-ioninput-password" name="password" type="password"
placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false">
</ion-input>
</core-show-password>
</ion-item>
<ion-grid class="ion-padding">
<ion-row>
<ion-col>
<ion-button expand="block" color="light" (click)="cancel($event)">{{ 'core.login.cancel' | translate }}</ion-button>
</ion-col>
<ion-col>
<ion-button type="submit" expand="block" [disabled]="!credForm.valid">{{ 'core.login.loginbutton' | translate }}</ion-button>
</ion-col>
</ion-row>
</ion-grid>
</form>
<!-- Forgotten password option. -->
<ion-list lines="none" *ngIf="showForgottenPassword && !isOAuth" class="core-login-forgotten-password ion-no-padding">
<ion-item button class="ion-text-center ion-text-wrap" (click)="forgottenPassword()" detail="false">
<ion-label>
{{ 'core.login.forgotten' | translate }}
</ion-label>
</ion-item>
</ion-list>
<!-- Identity providers. -->
<ion-list *ngIf="identityProviders && identityProviders.length" class="ion-padding-top core-login-identity-providers">
<ion-item class="ion-text-wrap" lines="none">
<ion-label><h3 class="item-heading">{{ 'core.login.potentialidps' | translate }}</h3></ion-label>
</ion-item>
<ion-item button *ngFor="let provider of identityProviders" class="ion-text-wrap core-oauth-icon"
(click)="oauthClicked(provider)" title="{{provider.name}}">
<img [src]="provider.iconurl" alt="" width="32" height="32" slot="start">
<ion-label>{{provider.name}}</ion-label>
</ion-item>
</ion-list>
<!-- If OAuth, display cancel button since the form isn't displayed. -->
<ion-list *ngIf="isOAuth">
<ion-button expand="block" class="ion-margin" color="light" (click)="cancel($event)">
{{ 'core.login.cancel' | translate }}
</ion-button>
</ion-list>
</ion-content>

View File

@ -0,0 +1,50 @@
// (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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreLoginReconnectPage } from './reconnect.page';
const routes: Routes = [
{
path: '',
component: CoreLoginReconnectPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CommonModule,
IonicModule,
TranslateModule.forChild(),
FormsModule,
ReactiveFormsModule,
CoreComponentsModule,
CoreDirectivesModule,
],
declarations: [
CoreLoginReconnectPage,
],
exports: [RouterModule],
})
export class CoreLoginReconnectPageModule {}

View File

@ -0,0 +1,245 @@
// (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, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { NavController } from '@ionic/angular';
import { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { CoreLoginHelper } from '@core/login/services/helper';
import { CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@classes/site';
import { CoreEvents } from '@singletons/events';
import { CoreError } from '@classes/errors/error';
/**
* Page to enter the user password to reconnect to a site.
*/
@Component({
selector: 'page-core-login-reconnect',
templateUrl: 'reconnect.html',
styleUrls: ['../../login.scss'],
})
export class CoreLoginReconnectPage implements OnInit, OnDestroy {
@ViewChild('reconnectForm') formElement?: ElementRef;
credForm: FormGroup;
siteUrl!: string;
username!: string;
userFullName!: string;
userAvatar?: string;
siteName!: string;
logoUrl?: string;
identityProviders?: CoreSiteIdentityProvider[];
showForgottenPassword = true;
showSiteAvatar = false;
isOAuth = false;
isLoggedOut: boolean;
siteId!: string;
protected page?: string;
protected pageParams?: Params;
protected siteConfig?: CoreSitePublicConfigResponse;
protected viewLeft = false;
protected eventThrown = false;
constructor(
protected navCtrl: NavController,
protected fb: FormBuilder,
protected route: ActivatedRoute,
) {
const currentSite = CoreSites.instance.getCurrentSite();
this.isLoggedOut = !!currentSite?.isLoggedOut();
this.credForm = fb.group({
password: ['', Validators.required],
});
}
/**
* Initialize the component.
*/
async ngOnInit(): Promise<void> {
const params = this.route.snapshot.queryParams;
this.siteId = params['siteId'];
this.page = params['pageName'];
this.pageParams = params['pageParams'];
try {
const site = await CoreSites.instance.getSite(this.siteId);
if (!site.infos) {
throw new CoreError('Invalid site');
}
this.username = site.infos.username;
this.userFullName = site.infos.fullname;
this.userAvatar = site.infos.userpictureurl;
this.siteUrl = site.infos.siteurl;
this.siteName = site.getSiteName();
// If login was OAuth we should only reach this page if the OAuth method ID has changed.
this.isOAuth = site.isOAuth();
// Show logo instead of avatar if it's a fixed site.
this.showSiteAvatar = !!this.userAvatar && !CoreLoginHelper.instance.getFixedSites();
const config = await CoreUtils.instance.ignoreErrors(site.getPublicConfig());
if (!config) {
return;
}
this.siteConfig = config;
await CoreSites.instance.checkRequiredMinimumVersion(config);
// Check logoURL if user avatar is not set.
if (this.userAvatar.startsWith(this.siteUrl + '/theme/image.php')) {
this.showSiteAvatar = false;
}
this.logoUrl = CoreLoginHelper.instance.getLogoUrl(config);
this.getDataFromConfig(this.siteConfig);
} catch (error) {
// Just leave the view.
this.cancel();
}
}
/**
* Component destroyed.
*/
ngOnDestroy(): void {
this.viewLeft = true;
CoreEvents.trigger(CoreEvents.LOGIN_SITE_UNCHECKED, { config: this.siteConfig }, this.siteId);
}
/**
* Get some data (like identity providers) from the site config.
*
* @param config Config to use.
*/
protected getDataFromConfig(config: CoreSitePublicConfigResponse): void {
const disabledFeatures = CoreLoginHelper.instance.getDisabledFeatures(config);
this.identityProviders = CoreLoginHelper.instance.getValidIdentityProviders(config, disabledFeatures);
this.showForgottenPassword = !CoreLoginHelper.instance.isForgottenPasswordDisabled(config);
if (!this.eventThrown && !this.viewLeft) {
this.eventThrown = true;
CoreEvents.trigger(CoreEvents.LOGIN_SITE_CHECKED, { config: config });
}
}
/**
* Cancel reconnect.
*
* @param e Event.
*/
cancel(e?: Event): void {
if (e) {
e.preventDefault();
e.stopPropagation();
}
CoreSites.instance.logout();
}
/**
* Tries to authenticate the user.
*
* @param e Event.
*/
async login(e: Event): Promise<void> {
e.preventDefault();
e.stopPropagation();
CoreApp.instance.closeKeyboard();
// Get input data.
const password = this.credForm.value.password;
if (!password) {
CoreDomUtils.instance.showErrorModal('core.login.passwordrequired', true);
return;
}
if (!CoreApp.instance.isOnline()) {
CoreDomUtils.instance.showErrorModal('core.networkerrormsg', true);
return;
}
const modal = await CoreDomUtils.instance.showModalLoading();
try {
// Start the authentication process.
const data = await CoreSites.instance.getUserToken(this.siteUrl, this.username, password);
await CoreSites.instance.updateSiteToken(this.siteUrl, this.username, data.token, data.privateToken);
CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, true);
// Update site info too.
await CoreSites.instance.updateSiteInfoByUrl(this.siteUrl, this.username);
// Reset fields so the data is not in the view anymore.
this.credForm.controls['password'].reset();
// Go to the site initial page.
await CoreLoginHelper.instance.goToSiteInitialPage({
redirectPage: this.page,
redirectParams: this.pageParams,
});
} catch (error) {
CoreLoginHelper.instance.treatUserTokenError(this.siteUrl, error, this.username, password);
if (error.loggedout) {
this.cancel();
} else if (error.errorcode == 'forcepasswordchangenotice') {
// Reset password field.
this.credForm.controls.password.reset();
}
} finally {
modal.dismiss();
}
}
/**
* Forgotten password button clicked.
*/
forgottenPassword(): void {
CoreLoginHelper.instance.forgottenPasswordClicked(this.siteUrl, this.username, this.siteConfig);
}
/**
* An OAuth button was clicked.
*
* @param provider The provider that was clicked.
*/
oauthClicked(provider: CoreSiteIdentityProvider): void {
if (!CoreLoginHelper.instance.openBrowserForOAuthLogin(this.siteUrl, provider, this.siteConfig?.launchurl)) {
CoreDomUtils.instance.showErrorModal('Invalid data.');
}
}
}

View File

@ -35,6 +35,7 @@ import { makeSingleton, Translate } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger';
import { CoreUrl } from '@singletons/url';
import { NavigationOptions } from '@ionic/angular/providers/nav-controller';
import { CoreObject } from '@/app/singletons/object';
/**
* Helper provider that provides some common features regarding authentication.
@ -126,7 +127,7 @@ export class CoreLoginHelperProvider {
const currentSite = CoreSites.instance.getCurrentSite();
const currentPage = CoreApp.instance.getCurrentPage();
if (!CoreApp.instance.isSSOAuthenticationOngoing() && currentSite?.isLoggedOut() && currentPage == 'login/reconnect') {
if (!CoreApp.instance.isSSOAuthenticationOngoing() && currentSite?.isLoggedOut() && currentPage == '/login/reconnect') {
// User must reauthenticate but he closed the InAppBrowser without doing so, logout him.
CoreSites.instance.logout();
}
@ -1106,14 +1107,11 @@ export class CoreLoginHelperProvider {
this.isOpeningReconnect = true;
await CoreUtils.instance.ignoreErrors(this.navCtrl.navigateRoot('/login/reconnect', {
queryParams: {
infoSiteUrl: info.siteurl,
siteUrl: result.siteUrl,
siteId: siteId,
queryParams: CoreObject.removeUndefined({
siteId,
pageName: data.pageName,
pageParams: data.params,
siteConfig: result.config,
},
}),
}));
this.isOpeningReconnect = false;

View File

@ -20,6 +20,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { CoreSettingsDeviceInfoPage } from './deviceinfo.page';
@ -38,6 +39,7 @@ const routes: Routes = [
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
],
declarations: [
CoreSettingsDeviceInfoPage,

View File

@ -14,7 +14,7 @@
import { Injectable } from '@angular/core';
import { NavController } from '@ionic/angular';
import { WKUserScriptWindow, WKUserScriptInjectionTime } from 'cordova-plugin-wkuserscript';
import { WKUserScriptWindow } from 'cordova-plugin-wkuserscript';
import { WKWebViewCookiesWindow } from 'cordova-plugin-wkwebview-cookies';
import { CoreApp } from '@services/app';
@ -470,7 +470,7 @@ export class CoreIframeUtilsProvider {
userScriptWindow.WKUserScript?.addScript({
id: 'CoreIframeUtilsRecaptchaScript',
file: recaptchaPath,
injectionTime: WKUserScriptInjectionTime.END,
injectionTime: userScriptWindow.WKUserScript?.InjectionTime.END,
});
// Handle post messages received by iframes.

View File

@ -0,0 +1,35 @@
// (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.
/**
* Singleton with helper functions for objects.
*/
export class CoreObject {
/**
* Delete all keys from an object whose value are null or undefined.
*
* @param object Object to modify.
*/
static removeUndefined<T>(object: T): T {
for (const name in object) {
if (object[name] === undefined) {
delete object[name];
}
}
return object;
}
}