Vmeda.Online/src/core/login/providers/helper.ts

973 lines
38 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable } from '@angular/core';
import { NavController, Platform } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '../../../providers/app';
import { CoreConfigProvider } from '../../../providers/config';
import { CoreEventsProvider } from '../../../providers/events';
import { CoreInitDelegate } from '../../../providers/init';
import { CoreLoggerProvider } from '../../../providers/logger';
import { CoreSitesProvider } from '../../../providers/sites';
import { CoreWSProvider } from '../../../providers/ws';
import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
import { CoreTextUtilsProvider } from '../../../providers/utils/text';
import { CoreUrlUtilsProvider } from '../../../providers/utils/url';
import { CoreUtilsProvider } from '../../../providers/utils/utils';
import { CoreConfigConstants } from '../../../configconstants';
import { CoreConstants } from '../../constants';
import { CoreEmulatorHelperProvider } from '../../emulator/providers/helper';
import { Md5 } from 'ts-md5/dist/md5';
export interface CoreLoginSSOData {
siteUrl?: string;
token?: string;
privateToken?: string;
pageName?: string;
pageParams?: any
};
/**
* Emulates the Cordova Zip plugin in desktop apps and in browser.
*/
@Injectable()
export class CoreLoginHelperProvider {
protected logger;
protected isSSOConfirmShown = false;
protected isOpenEditAlertShown = false;
protected navCtrl: NavController;
lastInAppUrl: string;
waitingForBrowser = false;
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider,
private wsProvider: CoreWSProvider, private translate: TranslateService, private textUtils: CoreTextUtilsProvider,
private eventsProvider: CoreEventsProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider,
private urlUtils: CoreUrlUtilsProvider,private configProvider: CoreConfigProvider, private platform: Platform,
private emulatorHelper: CoreEmulatorHelperProvider, private initDelegate: CoreInitDelegate) {
this.logger = logger.getInstance('CoreLoginHelper');
}
/**
* Accept site policy.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if success, rejected if failure.
*/
acceptSitePolicy(siteId?: string) : Promise<void> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.write('core_user_agree_site_policy', {}).then((result) => {
if (!result.status) {
// Error.
if (result.warnings && result.warnings.length) {
// Check if there is a warning 'alreadyagreed'.
for (let i in result.warnings) {
let warning = result.warnings[i];
if (warning.warningcode == 'alreadyagreed') {
// Policy already agreed, treat it as a success.
return;
}
}
// Another warning, reject.
return Promise.reject(result.warnings[0]);
} else {
return Promise.reject(null);
}
}
});
});
}
/**
* Function to handle URL received by Custom URL Scheme. If it's a SSO login, perform authentication.
*
* @param {string} url URL received.
* @return {boolean} True if it's a SSO URL, false otherwise.
*/
appLaunchedByURL(url: string) : boolean {
let ssoScheme = CoreConfigConstants.customurlscheme + '://token=';
if (url.indexOf(ssoScheme) == -1) {
return false;
}
if (this.appProvider.isSSOAuthenticationOngoing()) {
// Authentication ongoing, probably duplicated request.
return true;
}
if (this.appProvider.isDesktop()) {
// In desktop, make sure InAppBrowser is closed.
this.utils.closeInAppBrowser(true);
}
// App opened using custom URL scheme. Probably an SSO authentication.
this.appProvider.startSSOAuthentication();
this.logger.debug('App launched by URL');
// Delete the sso scheme from the URL.
url = url.replace(ssoScheme, '');
// Some platforms like Windows add a slash at the end. Remove it.
// Some sites add a # at the end of the URL. If it's there, remove it.
url = url.replace(/\/?#?\/?$/, '');
// Decode from base64.
try {
url = atob(url);
} catch(err) {
// Error decoding the parameter.
this.logger.error('Error decoding parameter received for login SSO');
return false;
}
let modal = this.domUtils.showModalLoading('mm.login.authenticating', true),
siteData: CoreLoginSSOData;
// Wait for app to be ready.
this.initDelegate.ready().then(() => {
return this.validateBrowserSSOLogin(url);
}).then((data) => {
siteData = data;
return this.handleSSOLoginAuthentication(siteData.siteUrl, siteData.token, siteData.privateToken);
}).then(() => {
if (siteData.pageName) {
// State defined, go to that state instead of site initial page.
this.navCtrl.push(siteData.pageName, siteData.pageParams);
} else {
this.goToSiteInitialPage(this.navCtrl, true);
}
}).catch((errorMessage) => {
if (typeof errorMessage == 'string' && errorMessage != '') {
this.domUtils.showErrorModal(errorMessage);
}
}).finally(() => {
modal.dismiss();
this.appProvider.finishSSOAuthentication();
});
return true;
}
/**
* Check if a site allows requesting a password reset through the app.
*
* @param {string} siteUrl URL of the site.
* @return {Promise<any>} Promise resolved with boolean: whether can be done through the app.
*/
canRequestPasswordReset(siteUrl: string) : Promise<any> {
return this.requestPasswordReset(siteUrl).then(() => {
return true;
}).catch((error) => {
return error.available == 1 || error.errorcode != 'invalidrecord';
});
}
/**
* Function called when an SSO InAppBrowser is closed or the app is resumed. Check if user needs to be logged out.
*/
checkLogout() {
if (!this.appProvider.isSSOAuthenticationOngoing() && this.sitesProvider.isLoggedIn() &&
this.sitesProvider.getCurrentSite().isLoggedOut() && this.navCtrl.getActive().name == 'CoreLoginReconnectPage') {
// User must reauthenticate but he closed the InAppBrowser without doing so, logout him.
this.sitesProvider.logout();
}
}
/**
* Show a confirm modal if needed and open a browser to perform SSO login.
*
* @param {string} siteurl URL of the site where the SSO login will be performed.
* @param {number} typeOfLogin CoreConstants.loginSSOCode or CoreConstants.loginSSOInAppCode.
* @param {string} [service] The service to use. If not defined, external service will be used.
* @param {string} [launchUrl] The URL to open for SSO. If not defined, local_mobile launch URL will be used.
* @return {Void}
*/
confirmAndOpenBrowserForSSOLogin(siteUrl: string, typeOfLogin: number, service?: string, launchUrl?: string) : void {
// Show confirm only if it's needed. Treat "false" (string) as false to prevent typing errors.
let showConfirmation = this.shouldShowSSOConfirm(typeOfLogin),
promise;
if (showConfirmation) {
promise = this.domUtils.showConfirm(this.translate.instant('mm.login.logininsiterequired'));
} else {
promise = Promise.resolve();
}
promise.then(() => {
this.openBrowserForSSOLogin(siteUrl, typeOfLogin, service, launchUrl);
});
}
/**
* Format profile fields, filtering the ones that shouldn't be shown on signup and classifying them in categories.
*
* @param {any[]} profileFields Profile fields to format.
* @return {any} Categories with the fields to show in each one.
*/
formatProfileFieldsForSignup(profileFields: any[]) : any {
let categories = {};
if (!profileFields) {
return categories;
}
profileFields.forEach((field) => {
if (!field.signup) {
// Not a signup field, ignore it.
return;
}
if (!categories[field.categoryid]) {
categories[field.categoryid] = {
id: field.categoryid,
name: field.categoryname,
fields: []
}
}
categories[field.categoryid].fields.push(field);
});
return categories;
}
/**
* Builds an object with error messages for some common errors.
* Please notice that this function doesn't support all possible error types.
*
* @param {string} [requiredMsg] Code of the string for required error.
* @param {string} [emailMsg] Code of the string for invalid email error.
* @param {string} [patternMsg] Code of the string for pattern not match error.
* @param {string} [urlMsg] Code of the string for invalid url error.
* @param {string} [minlengthMsg] Code of the string for "too short" error.
* @param {string} [maxlengthMsg] Code of the string for "too long" error.
* @param {string} [minMsg] Code of the string for min value error.
* @param {string} [maxMsg] Code of the string for max value error.
* @return {any} Object with the errors.
*/
getErrorMessages(requiredMsg?: string, emailMsg?: string, patternMsg?: string, urlMsg?: string, minlengthMsg?: string,
maxlengthMsg?: string, minMsg?: string, maxMsg?: string) : any {
var errors: any = {};
if (requiredMsg) {
errors.required = errors.requiredTrue = this.translate.instant(requiredMsg);
}
if (emailMsg) {
errors.email = this.translate.instant(emailMsg);
}
if (patternMsg) {
errors.pattern = this.translate.instant(patternMsg);
}
if (urlMsg) {
errors.url = this.translate.instant(urlMsg);
}
if (minlengthMsg) {
errors.minlength = this.translate.instant(minlengthMsg);
}
if (maxlengthMsg) {
errors.maxlength = this.translate.instant(maxlengthMsg);
}
if (minMsg) {
errors.min = this.translate.instant(minMsg);
}
if (maxMsg) {
errors.max = this.translate.instant(maxMsg);
}
return errors;
}
/**
* Get the site policy.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<string>} Promise resolved with the site policy.
*/
getSitePolicy(siteId?: string) : Promise<string> {
return this.sitesProvider.getSite(siteId).then((site) => {
// Check if it's stored in the site config.
let sitePolicy = site.getStoredConfig('sitepolicy');
if (typeof sitePolicy != 'undefined') {
return sitePolicy ? sitePolicy : Promise.reject(null);
}
// Not in the config, try to get it using auth_email_get_signup_settings.
return this.wsProvider.callAjax('auth_email_get_signup_settings', {}, {siteUrl: site.getURL()}).then((settings) => {
return settings.sitepolicy ? settings.sitepolicy : Promise.reject(null);
});
});
}
/**
* Get fixed site or sites.
*
* @return {string|any[]} Fixed site or list of fixed sites.
*/
getFixedSites() : string|any[] {
return CoreConfigConstants.siteurl;
}
/**
* Get the valid identity providers from a site config.
*
* @param {any} siteConfig Site's public config.
* @return {any[]} Valid identity providers.
*/
getValidIdentityProviders(siteConfig: any) : any[] {
let validProviders = [],
httpUrl = this.textUtils.concatenatePaths(siteConfig.wwwroot, 'auth/oauth2/'),
httpsUrl = this.textUtils.concatenatePaths(siteConfig.httpswwwroot, 'auth/oauth2/');
if (siteConfig.identityproviders && siteConfig.identityproviders.length) {
siteConfig.identityproviders.forEach((provider) => {
if (provider.url && (provider.url.indexOf(httpsUrl) != -1 || provider.url.indexOf(httpUrl) != -1)) {
validProviders.push(provider);
}
});
}
return validProviders;
}
/**
* Go to the page to add a new site.
* If a fixed URL is configured, go to credentials instead.
*
* @param {NavController} navCtrl The NavController instance to use.
* @param {boolean} [setRoot] True to set the new page as root, false to add it to the stack.
* @return {Promise<any>} Promise resolved when done.
*/
goToAddSite(navCtrl: NavController, setRoot?: boolean) : Promise<any> {
let pageName,
params;
if (this.isFixedUrlSet()) {
// Fixed URL is set, go to credentials page.
let url = typeof CoreConfigConstants.siteurl == 'string' ?
CoreConfigConstants.siteurl : CoreConfigConstants.siteurl[0].url;
pageName = 'CoreLoginCredentialsPage';
params = {siteUrl: url};
} else {
pageName = 'CoreLoginSitePage';
}
if (setRoot) {
return navCtrl.setRoot(pageName, params, {animate: false});
} else {
return navCtrl.push(pageName, params);
}
}
/**
* Go to the initial page of a site depending on 'userhomepage' setting.
*
* @param {NavController} navCtrl The NavController instance to use.
* @param {boolean} [setRoot] True to set the new page as root, false to add it to the stack.
* @return {Promise<any>} Promise resolved when done.
*/
goToSiteInitialPage(navCtrl: NavController, setRoot?: boolean) : Promise<any> {
return this.isMyOverviewEnabled().then((myOverview) => {
let myCourses = !myOverview && this.isMyCoursesEnabled(),
site = this.sitesProvider.getCurrentSite(),
promise;
if (!site) {
return Promise.reject(null);
}
// Check if frontpage is needed to be shown. (If configured or if any of the other avalaible).
if ((site.getInfo() && site.getInfo().userhomepage === 0) || (!myCourses && !myOverview)) {
promise = this.isFrontpageEnabled();
} else {
promise = Promise.resolve(false);
}
return promise.then((frontpage) => {
// Check avalaibility in priority order.
let pageName,
params;
// @todo Use real pages names when they are implemented.
if (frontpage) {
pageName = 'Frontpage';
} else if (myOverview) {
pageName = 'MyOverview';
} else if (myCourses) {
pageName = 'MyCourses';
} else {
// Anything else available, go to the user profile.
pageName = 'User';
params = {
userId: site.getUserId()
};
}
if (setRoot) {
return navCtrl.setRoot(pageName, params, {animate: false});
} else {
return navCtrl.push(pageName, params);
}
});
});
}
/**
* Convenient helper to handle authentication in the app using a token received by SSO login. If it's a new account,
* the site is stored and the user is authenticated. If the account already exists, update its token.
*
* @param {string} siteUrl Site's URL.
* @param {string} token User's token.
* @param {string} [privateToken] User's private token.
* @return {Promise<any>} Promise resolved when the user is authenticated with the token.
*/
handleSSOLoginAuthentication(siteUrl: string, token: string, privateToken?: string) : Promise<any> {
if (this.sitesProvider.isLoggedIn()) {
// User logged in, he is reconnecting. Retrieve username.
var info = this.sitesProvider.getCurrentSite().getInfo();
if (typeof info != 'undefined' && typeof info.username != 'undefined') {
return this.sitesProvider.updateSiteToken(info.siteurl, info.username, token, privateToken).then(() => {
this.sitesProvider.updateSiteInfoByUrl(info.siteurl, info.username);
}).catch(() => {
// Error updating token, return proper error message.
return Promise.reject(this.translate.instant('mm.login.errorupdatesite'));
});
}
return Promise.reject(this.translate.instant('mm.login.errorupdatesite'));
} else {
return this.sitesProvider.newSite(siteUrl, token, privateToken);
}
}
/**
* Check if the app is configured to use several fixed URLs.
*
* @return {boolean} Whether there are several fixed URLs.
*/
hasSeveralFixedSites() : boolean {
return CoreConfigConstants.siteurl && Array.isArray(CoreConfigConstants.siteurl) &&
CoreConfigConstants.siteurl.length > 1;
}
/**
* Function called when a page starts loading in any InAppBrowser window.
*
* @param {string} url Loaded url.
*/
inAppBrowserLoadStart(url: string) : void {
// URLs with a custom scheme can be prefixed with "http://" or "https://", we need to remove this.
url = url.replace(/^https?:\/\//, '');
if (this.appLaunchedByURL(url)) {
// Close the browser if it's a valid SSO URL.
this.utils.closeInAppBrowser(false);
} else if (this.platform.is('android')) {
// Check if the URL has a custom URL scheme. In Android they need to be opened manually.
const urlScheme = this.urlUtils.getUrlProtocol(url);
if (urlScheme && urlScheme !== 'file' && urlScheme !== 'cdvfile') {
// Open in browser should launch the right app if found and do nothing if not found.
this.utils.openInBrowser(url);
// At this point the InAppBrowser is showing a "Webpage not available" error message.
// Try to navigate to last loaded URL so this error message isn't found.
if (this.lastInAppUrl) {
this.utils.openInApp(this.lastInAppUrl);
} else {
// No last URL loaded, close the InAppBrowser.
this.utils.closeInAppBrowser(false);
}
} else {
this.lastInAppUrl = url;
}
}
}
/**
* Given a site public config, check if email signup is disabled.
*
* @param {any} config Site public config.
* @return {boolean} Whether email signup is disabled.
*/
isEmailSignupDisabled(config: any) : boolean {
let disabledFeatures = config && config.tool_mobile_disabledfeatures;
if (!disabledFeatures) {
return false;
}
let regEx = new RegExp('(,|^)\\$mmLoginEmailSignup(,|$)', 'g');
return !!disabledFeatures.match(regEx);
}
/**
* Check if the app is configured to use a fixed URL (only 1).
*
* @return {boolean} Whether there is 1 fixed URL.
*/
isFixedUrlSet() : boolean {
if (Array.isArray(CoreConfigConstants.siteurl)) {
return CoreConfigConstants.siteurl.length == 1;
}
return !!CoreConfigConstants.siteurl;
}
/**
* Check if the app is configured to use a fixed URL (only 1).
*
* @return {Promise<boolean>} Promise resolved with boolean: whether there is 1 fixed URL.
*/
protected isFrontpageEnabled() : Promise<boolean> {
// var $mmaFrontpage = $mmAddonManager.get('$mmaFrontpage');
// if ($mmaFrontpage && !$mmaFrontpage.isDisabledInSite()) {
// return $mmaFrontpage.isFrontpageAvailable().then(() => {
// return true;
// }).catch(() => {
// return false;
// });
// }
// @todo: Implement it when front page is implemented.
return Promise.resolve(false);
}
/**
* Check if My Courses is enabled.
*
* @return {boolean} Whether My Courses is enabled.
*/
protected isMyCoursesEnabled() : boolean {
// @todo: Implement it when My Courses is implemented.
return false;
// return !$mmCourses.isMyCoursesDisabledInSite();
}
/**
* Check if My Overview is enabled.
*
* @return {Promise<boolean>} Promise resolved with boolean: whether My Overview is enabled.
*/
protected isMyOverviewEnabled() : Promise<boolean> {
// @todo: Implement it when My Overview is implemented.
return Promise.resolve(false);
}
/**
* Check if current site is logged out, triggering mmCoreEventSessionExpired if it is.
*
* @param {string} [pageName] Name of the page to go once authenticated if logged out. If not defined, site initial page.
* @param {any} [params] Params of the page to go once authenticated if logged out.
* @return {boolean} True if user is logged out, false otherwise.
*/
isSiteLoggedOut(pageName?: string, params?: any) : boolean {
let site = this.sitesProvider.getCurrentSite();
if (!site) {
return false;
}
if (site.isLoggedOut()) {
this.eventsProvider.trigger(CoreEventsProvider.SESSION_EXPIRED, {
siteId: site.getId(),
pageName: pageName,
params: params
});
return true;
}
return false;
}
/**
* Check if SSO login should use an embedded browser.
*
* @param {number} code Code to check.
* @return {boolean} True if embedded browser, false othwerise.
*/
isSSOEmbeddedBrowser(code: number) : boolean {
if (this.appProvider.isDesktop() && this.emulatorHelper.isLinux()) {
// In Linux desktop apps, always use embedded browser.
return true;
}
return code == CoreConstants.loginSSOInAppCode;
}
/**
* Check if SSO login is needed based on code returned by the WS.
*
* @param {number} code Code to check.
* @return {boolean} True if SSO login is needed, false othwerise.
*/
isSSOLoginNeeded(code: number) : boolean {
return code == CoreConstants.loginSSOCode || code == CoreConstants.loginSSOInAppCode;
}
/**
* Open a browser to perform OAuth login (Google, Facebook, Microsoft).
*
* @param {string} siteUrl URL of the site where the login will be performed.
* @param {any} provider The identity provider.
* @param {string} [launchUrl] The URL to open for SSO. If not defined, tool/mobile launch URL will be used.
* @param {string} [pageName] Name of the page to go once authenticated. If not defined, site initial page.
* @param {any} [pageParams] Params of the state to go once authenticated.
* @return {boolean} True if success, false if error.
*/
openBrowserForOAuthLogin(siteUrl: string, provider: any, launchUrl?: string, pageName?: string, pageParams?: any) : boolean {
launchUrl = launchUrl || siteUrl + '/admin/tool/mobile/launch.php';
if (!provider || !provider.url) {
return false;
}
let service = this.sitesProvider.determineService(siteUrl),
loginUrl = this.prepareForSSOLogin(siteUrl, service, launchUrl, pageName, pageParams),
params = this.urlUtils.extractUrlParams(provider.url);
if (!params.id) {
return false;
}
loginUrl += '&oauthsso=' + params.id;
if (this.appProvider.isDesktop() && this.emulatorHelper.isLinux()) {
// In Linux desktop apps, always use embedded browser.
this.utils.openInApp(loginUrl);
} else {
// Always open it in browser because the user might have the session stored in there.
this.utils.openInBrowser(loginUrl);
if ((<any>navigator).app) {
(<any>navigator).app.exitApp();
}
}
return true;
}
/**
* Open a browser to perform SSO login.
*
* @param {string} siteurl URL of the site where the SSO login will be performed.
* @param {number} typeOfLogin CoreConstants.loginSSOCode or CoreConstants.loginSSOInAppCode.
* @param {string} [service] The service to use. If not defined, external service will be used.
* @param {string} [launchUrl] The URL to open for SSO. If not defined, local_mobile launch URL will be used.
* @param {string} [pageName] Name of the page to go once authenticated. If not defined, site initial page.
* @param {any} [pageParams] Params of the state to go once authenticated.
*/
openBrowserForSSOLogin(siteUrl: string, typeOfLogin: number, service?: string, launchUrl?: string, pageName?: string,
pageParams?: any) : void {
let loginUrl = this.prepareForSSOLogin(siteUrl, service, launchUrl, pageName, pageParams);
if (this.isSSOEmbeddedBrowser(typeOfLogin)) {
let options = {
clearsessioncache: 'yes', // Clear the session cache to allow for multiple logins.
closebuttoncaption: this.translate.instant('mm.login.cancel'),
}
this.utils.openInApp(loginUrl, options);
} else {
this.utils.openInBrowser(loginUrl);
if ((<any>navigator).app) {
(<any>navigator).app.exitApp();
}
}
}
/**
* Convenient helper to open change password page.
*
* @param {string} siteUrl Site URL to construct change password URL.
* @param {string} error Error message.
*/
openChangePassword(siteUrl: string, error: string) : void {
let alert = this.domUtils.showAlert(this.translate.instant('mm.core.notice'), error, null, 3000);
alert.onDidDismiss(() => {
this.utils.openInApp(siteUrl + '/login/change_password.php');
});
}
/**
* Open forgotten password in inappbrowser.
*
* @param {string} siteUrl URL of the site.
*/
openForgottenPassword(siteUrl: string) : void {
this.utils.openInApp(siteUrl + '/login/forgot_password.php');
}
/*
* Function to open in app browser to change password or complete user profile.
*
* @param {string} siteId The site ID.
* @param {string} path The relative path of the URL to open.
* @param {string} alertMessage The key of the message to display before opening the in app browser.
* @param {boolean} [invalidateCache] Whether to invalidate site's cache (e.g. when the user is forced to change password).
*/
openInAppForEdit(siteId: string, path: string, alertMessage: string, invalidateCache?: boolean) : void {
if (!siteId || siteId !== this.sitesProvider.getCurrentSiteId()) {
// Site that triggered the event is not current site, nothing to do.
return;
}
let currentSite = this.sitesProvider.getCurrentSite(),
siteUrl = currentSite && currentSite.getURL();
if (!currentSite || !siteUrl) {
return;
}
if (!this.isOpenEditAlertShown && !this.waitingForBrowser) {
this.isOpenEditAlertShown = true;
if (invalidateCache) {
currentSite.invalidateWsCache();
}
// Open change password.
alertMessage = this.translate.instant(alertMessage) + '<br>' + this.translate.instant('mm.core.redirectingtosite');
currentSite.openInAppWithAutoLogin(siteUrl + path, undefined, alertMessage).then(() => {
this.waitingForBrowser = true;
}).finally(() => {
this.isOpenEditAlertShown = false;
});
}
}
/**
* Prepare the app to perform SSO login.
*
* @param {string} siteUrl URL of the site where the SSO login will be performed.
* @param {string} [service] The service to use. If not defined, external service will be used.
* @param {string} [launchUrl] The URL to open for SSO. If not defined, local_mobile launch URL will be used.
* @param {string} [pageName] Name of the page to go once authenticated. If not defined, site initial page.
* @param {any} [pageParams] Params of the state to go once authenticated.
*/
prepareForSSOLogin(siteUrl: string, service?: string, launchUrl?: string, pageName?: string, pageParams?: any) : string {
service = service || CoreConfigConstants.wsextservice;
launchUrl = launchUrl || siteUrl + '/local/mobile/launch.php';
let passport = Math.random() * 1000,
loginUrl = launchUrl + '?service=' + service;
loginUrl += "&passport=" + passport;
loginUrl += "&urlscheme=" + CoreConfigConstants.customurlscheme;
// Store the siteurl and passport in $mmConfig for persistence. We are "configuring"
// the app to wait for an SSO. $mmConfig shouldn't be used as a temporary storage.
this.configProvider.set(CoreConstants.loginLaunchData, JSON.stringify({
siteUrl: siteUrl,
passport: passport,
pageName: pageName || '',
pageParams: pageParams || {}
}));
return loginUrl;
}
/**
* Request a password reset.
*
* @param {string} siteUrl URL of the site.
* @param {string} [username] Username to search.
* @param {string} [email] Email to search.
* @return {Promise<any>} Promise resolved when done.
*/
requestPasswordReset(siteUrl: string, username?: string, email?: string) : Promise<any> {
var params: any = {};
if (username) {
params.username = username;
}
if (email) {
params.email = email;
}
return this.wsProvider.callAjax('core_auth_request_password_reset', params, {siteUrl: siteUrl});
}
/**
* Function that should be called when the session expires. Reserved for core use.
*
* @param {any} data Data received by the SESSION_EXPIRED event.
*/
sessionExpired(data: any) : void {
let siteId = data && data.siteId,
currentSite = this.sitesProvider.getCurrentSite(),
siteUrl = currentSite && currentSite.getURL(),
promise;
if (!currentSite || !siteUrl) {
return;
}
if (siteId && siteId !== currentSite.getId()) {
return; // Site that triggered the event is not current site.
}
// Check authentication method.
this.sitesProvider.checkSite(siteUrl).then((result) => {
if (result.warning) {
this.domUtils.showErrorModal(result.warning, true, 4000);
}
if (this.isSSOLoginNeeded(result.code)) {
// SSO. User needs to authenticate in a browser. Prevent showing the message several times
// or show it again if the user is already authenticating using SSO.
if (!this.appProvider.isSSOAuthenticationOngoing() && !this.isSSOConfirmShown && !this.waitingForBrowser) {
this.isSSOConfirmShown = true;
if (this.shouldShowSSOConfirm(result.code)) {
promise = this.domUtils.showConfirm(this.translate.instant('mm.login.' +
(currentSite.isLoggedOut() ? 'loggedoutssodescription' : 'reconnectssodescription')));
} else {
promise = Promise.resolve();
}
promise.then(() => {
this.waitingForBrowser = true;
this.openBrowserForSSOLogin(result.siteUrl, result.code, result.service,
result.config && result.config.launchurl, data.pageName, data.params);
}).catch(() => {
// User cancelled, logout him.
this.sitesProvider.logout();
}).finally(() => {
this.isSSOConfirmShown = false;
});
}
} else {
let info = currentSite.getInfo();
if (typeof info != 'undefined' && typeof info.username != 'undefined') {
this.navCtrl.setRoot('CoreLoginReconnectPage', {
infoSiteUrl: info.siteurl,
siteUrl: result.siteUrl,
siteId: siteId,
pageName: data.pageName,
pageParams: data.params,
siteConfig: result.config
});
}
}
}).catch((error) => {
// Error checking site.
if (currentSite.isLoggedOut()) {
// Site is logged out, show error and logout the user.
this.domUtils.showErrorModalDefault(error, 'mm.core.networkerrormsg', true);
this.sitesProvider.logout();
}
});
}
/**
* Set a NavController to use.
*
* @param {NavController} navCtrl Nav controller.
*/
setNavCtrl(navCtrl: NavController) : void {
this.navCtrl = navCtrl;
}
/**
* Check if a confirm should be shown to open a SSO authentication.
*
* @param {number} typeOfLogin CoreConstants.loginSSOCode or CoreConstants.loginSSOInAppCode.
* @return {boolean} True if confirm modal should be shown, false otherwise.
*/
shouldShowSSOConfirm(typeOfLogin: number) : boolean {
return !this.isSSOEmbeddedBrowser(typeOfLogin) &&
(!CoreConfigConstants.skipssoconfirmation || String(CoreConfigConstants.skipssoconfirmation) === 'false');
}
/**
* Function called when site policy is not agreed. Reserved for core use.
*
* @param {string} [siteId] Site ID. If not defined, current site.
*/
sitePolicyNotAgreed(siteId?: string) : void {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
if (!siteId || siteId != this.sitesProvider.getCurrentSiteId()) {
// Only current site allowed.
return;
}
if (!this.sitesProvider.getCurrentSite().wsAvailable('core_user_agree_site_policy')) {
// WS not available, stop.
return;
}
this.navCtrl.setRoot('CoreLoginSitePolicyPage', {siteId: siteId});
}
/**
* Convenient helper to handle get User Token error. It redirects to change password page if forcepassword is set.
*
* @param {string} siteUrl Site URL to construct change password URL.
* @param {any} error Error object containing errorcode and error message.
*/
treatUserTokenError(siteUrl: string, error: any) : void {
if (typeof error == 'string') {
this.domUtils.showErrorModal(error);
} else if (error.errorcode == 'forcepasswordchangenotice') {
this.openChangePassword(siteUrl, error.error);
} else {
this.domUtils.showErrorModal(error.error);
}
}
/**
* Convenient helper to validate a browser SSO login.
*
* @param {string} url URL received, to be validated.
* @return {Promise<CoreLoginSSOData>} Promise resolved on success.
*/
validateBrowserSSOLogin(url: string) : Promise<CoreLoginSSOData> {
// Split signature:::token
const params = url.split(":::");
return this.configProvider.get(CoreConstants.loginLaunchData).then((data): any => {
try {
data = JSON.parse(data);
} catch(ex) {
return Promise.reject(null);
}
let launchSiteURL = data.siteurl,
passport = data.passport;
// Reset temporary values.
this.configProvider.delete(CoreConstants.loginLaunchData);
// Validate the signature.
// We need to check both http and https.
let signature = <string>Md5.hashAsciiStr(launchSiteURL + passport);
if (signature != params[0]) {
if (launchSiteURL.indexOf("https://") != -1) {
launchSiteURL = launchSiteURL.replace("https://", "http://");
} else {
launchSiteURL = launchSiteURL.replace("http://", "https://");
}
signature = <string>Md5.hashAsciiStr(launchSiteURL + passport);
}
if (signature == params[0]) {
this.logger.debug('Signature validated');
return {
siteUrl: launchSiteURL,
token: params[1],
privateToken: params[2],
pageName: data.pageName,
pageParams: data.pageParams
}
} else {
this.logger.debug('Invalid signature in the URL request yours: ' + params[0] + ' mine: '
+ signature + ' for passport ' + passport);
return Promise.reject(this.translate.instant('mm.core.unexpectederror'));
}
});
}
}