MOBILE-2995 login: Go to credentials if scan authentication QR fails

main
Dani Palou 2020-04-17 10:48:06 +02:00
parent f9df95c727
commit 2b57c9485d
8 changed files with 347 additions and 226 deletions

View File

@ -22,7 +22,7 @@ import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCustomURLSchemesProvider } from '@providers/urlschemes'; import { CoreCustomURLSchemesProvider, CoreCustomURLSchemesHandleError } from '@providers/urlschemes';
import { CoreLoginHelperProvider } from '@core/login/providers/helper'; import { CoreLoginHelperProvider } from '@core/login/providers/helper';
import { Keyboard } from '@ionic-native/keyboard'; import { Keyboard } from '@ionic-native/keyboard';
import { ScreenOrientation } from '@ionic-native/screen-orientation'; import { ScreenOrientation } from '@ionic-native/screen-orientation';
@ -140,7 +140,9 @@ export class MoodleMobileApp implements OnInit {
if (this.urlSchemesProvider.isCustomURL(url)) { if (this.urlSchemesProvider.isCustomURL(url)) {
// Close the browser if it's a valid SSO URL. // Close the browser if it's a valid SSO URL.
this.urlSchemesProvider.handleCustomURL(url); this.urlSchemesProvider.handleCustomURL(url).catch((error: CoreCustomURLSchemesHandleError) => {
this.urlSchemesProvider.treatHandleCustomURLError(error);
});
this.utils.closeInAppBrowser(false); this.utils.closeInAppBrowser(false);
} else if (this.platform.is('android')) { } else if (this.platform.is('android')) {
@ -194,7 +196,9 @@ export class MoodleMobileApp implements OnInit {
this.lastUrls[url] = Date.now(); this.lastUrls[url] = Date.now();
this.eventsProvider.trigger(CoreEventsProvider.APP_LAUNCHED_URL, url); this.eventsProvider.trigger(CoreEventsProvider.APP_LAUNCHED_URL, url);
this.urlSchemesProvider.handleCustomURL(url); this.urlSchemesProvider.handleCustomURL(url).catch((error: CoreCustomURLSchemesHandleError) => {
this.urlSchemesProvider.treatHandleCustomURLError(error);
});
}); });
}; };

View File

@ -1789,6 +1789,7 @@
"core.login.usernotaddederror": "User not added - error", "core.login.usernotaddederror": "User not added - error",
"core.login.visitchangepassword": "Do you want to visit the site to change the password?", "core.login.visitchangepassword": "Do you want to visit the site to change the password?",
"core.login.webservicesnotenabled": "Your host site may not have enabled Web services. Please contact your administrator for help.", "core.login.webservicesnotenabled": "Your host site may not have enabled Web services. Please contact your administrator for help.",
"core.login.youcanstillconnectwithcredentials": "You can still connect to the site by entering your username and password.",
"core.login.yourenteredsite": "Connect to your site", "core.login.yourenteredsite": "Connect to your site",
"core.lostconnection": "Your authentication token is invalid or has expired. You will have to reconnect to the site.", "core.lostconnection": "Your authentication token is invalid or has expired. You will have to reconnect to the site.",
"core.mainmenu.changesite": "Change site", "core.mainmenu.changesite": "Change site",

View File

@ -100,6 +100,7 @@
"usernamerequired": "Username required", "usernamerequired": "Username required",
"usernotaddederror": "User not added - error", "usernotaddederror": "User not added - error",
"visitchangepassword": "Do you want to visit the site to change the password?", "visitchangepassword": "Do you want to visit the site to change the password?",
"yourenteredsite": "Connect to your site", "webservicesnotenabled": "Your host site may not have enabled Web services. Please contact your administrator for help.",
"webservicesnotenabled": "Your host site may not have enabled Web services. Please contact your administrator for help." "youcanstillconnectwithcredentials": "You can still connect to the site by entering your username and password.",
"yourenteredsite": "Connect to your site"
} }

View File

@ -17,10 +17,11 @@ import { IonicPage, NavController, ModalController, AlertController, NavParams }
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider, CoreSiteCheckResponse, CoreLoginSiteInfo } from '@providers/sites'; import { CoreSitesProvider, CoreSiteCheckResponse, CoreLoginSiteInfo } from '@providers/sites';
import { CoreCustomURLSchemesProvider } from '@providers/urlschemes'; import { CoreCustomURLSchemesProvider, CoreCustomURLSchemesHandleError } from '@providers/urlschemes';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreConfigConstants } from '../../../../configconstants'; import { CoreConfigConstants } from '../../../../configconstants';
import { CoreLoginHelperProvider } from '../../providers/helper'; import { CoreLoginHelperProvider } from '../../providers/helper';
import { FormBuilder, FormGroup, ValidatorFn, AbstractControl } from '@angular/forms'; import { FormBuilder, FormGroup, ValidatorFn, AbstractControl } from '@angular/forms';
@ -74,7 +75,8 @@ export class CoreLoginSitePage {
protected eventsProvider: CoreEventsProvider, protected eventsProvider: CoreEventsProvider,
protected translate: TranslateService, protected translate: TranslateService,
protected utils: CoreUtilsProvider, protected utils: CoreUtilsProvider,
private urlSchemesProvider: CoreCustomURLSchemesProvider) { protected urlSchemesProvider: CoreCustomURLSchemesProvider,
protected textUtils: CoreTextUtilsProvider) {
this.showKeyboard = !!navParams.get('showKeyboard'); this.showKeyboard = !!navParams.get('showKeyboard');
this.showScanQR = this.utils.canScanQR(); this.showScanQR = this.utils.canScanQR();
@ -363,19 +365,69 @@ export class CoreLoginSitePage {
/** /**
* Scan a QR code and put its text in the URL input. * Scan a QR code and put its text in the URL input.
*/ */
scanQR(): void { async scanQR(): Promise<void> {
// Scan for a QR code. // Scan for a QR code.
this.utils.scanQR().then((text) => { const text = await this.utils.scanQR();
if (text) {
if (this.urlSchemesProvider.isCustomURL(text)) {
this.urlSchemesProvider.handleCustomURL(text);
} else {
// Not a custom URL scheme, put the text in the field.
this.siteForm.controls.siteUrl.setValue(text);
this.connect(new Event('click'), text); if (text) {
if (this.urlSchemesProvider.isCustomURL(text)) {
try {
await this.urlSchemesProvider.handleCustomURL(text);
} catch (error) {
if (error && error.data && error.data.isAuthenticationURL && error.data.siteUrl) {
// An error ocurred, but it's an authentication URL and we have the site URL.
this.treatErrorInAuthenticationCustomURL(text, error);
} else {
this.urlSchemesProvider.treatHandleCustomURLError(error);
}
} }
} else {
// Not a custom URL scheme, put the text in the field.
this.siteForm.controls.siteUrl.setValue(text);
this.connect(new Event('click'), text);
} }
}); }
}
/**
* Treat an error while handling a custom URL meant to perform an authentication.
* If the site doesn't use SSO, the user will be sent to the credentials screen.
*
* @param customURL Custom URL handled.
* @param error Error data.
* @return Promise resolved when done.
*/
protected async treatErrorInAuthenticationCustomURL(customURL: string, error: CoreCustomURLSchemesHandleError): Promise<void> {
const siteUrl = error.data.siteUrl;
const modal = this.domUtils.showModalLoading();
// Set the site URL in the input.
this.siteForm.controls.siteUrl.setValue(siteUrl);
try {
// Check if site uses SSO.
const response = await this.sitesProvider.checkSite(siteUrl);
await this.sitesProvider.checkRequiredMinimumVersion(response.config);
if (!this.loginHelper.isSSOLoginNeeded(response.code)) {
// No SSO, go to credentials page.
await this.navCtrl.push('CoreLoginCredentialsPage', {
siteUrl: response.siteUrl,
siteConfig: response.config,
});
}
} catch (error) {
// Ignore errors.
} finally {
modal.dismiss();
}
// Now display the error.
error.error = this.textUtils.addTextToError(error.error,
'<br><br>' + this.translate.instant('core.login.youcanstillconnectwithcredentials'));
this.urlSchemesProvider.treatHandleCustomURLError(error);
} }
} }

View File

@ -16,7 +16,7 @@ import { Component, OnDestroy } from '@angular/core';
import { IonicPage, NavController } from 'ionic-angular'; import { IonicPage, NavController } from 'ionic-angular';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreCustomURLSchemesProvider } from '@providers/urlschemes'; import { CoreCustomURLSchemesProvider, CoreCustomURLSchemesHandleError } from '@providers/urlschemes';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../providers/delegate'; import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../providers/delegate';
@ -179,7 +179,9 @@ export class CoreMainMenuMorePage implements OnDestroy {
if (text) { if (text) {
if (this.urlSchemesProvider.isCustomURL(text)) { if (this.urlSchemesProvider.isCustomURL(text)) {
// Is a custom URL scheme, handle it. // Is a custom URL scheme, handle it.
this.urlSchemesProvider.handleCustomURL(text); this.urlSchemesProvider.handleCustomURL(text).catch((error: CoreCustomURLSchemesHandleError) => {
this.urlSchemesProvider.treatHandleCustomURLError(error);
});
} else if (/^[^:]{2,}:\/\/[^ ]+$/i.test(text)) { // Check if it's a URL. } else if (/^[^:]{2,}:\/\/[^ ]+$/i.test(text)) { // Check if it's a URL.
// Check if the app can handle the URL. // Check if the app can handle the URL.
this.linkHelper.handleLink(text, undefined, this.navCtrl, true, true).then((treated) => { this.linkHelper.handleLink(text, undefined, this.navCtrl, true, true).then((treated) => {

View File

@ -21,7 +21,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreCustomURLSchemesProvider } from '@providers/urlschemes'; import { CoreCustomURLSchemesProvider, CoreCustomURLSchemesHandleError } from '@providers/urlschemes';
/** /**
* Directive to open a link in external browser. * Directive to open a link in external browser.
@ -113,7 +113,9 @@ export class CoreLinkDirective implements OnInit {
this.domUtils.scrollToElementBySelector(this.content, '#' + href + ', [name=\'' + href + '\']'); this.domUtils.scrollToElementBySelector(this.content, '#' + href + ', [name=\'' + href + '\']');
} }
} else if (this.urlSchemesProvider.isCustomURL(href)) { } else if (this.urlSchemesProvider.isCustomURL(href)) {
this.urlSchemesProvider.handleCustomURL(href); this.urlSchemesProvider.handleCustomURL(href).catch((error: CoreCustomURLSchemesHandleError) => {
this.urlSchemesProvider.treatHandleCustomURLError(error);
});
} else { } else {
// It's an external link, we will open with browser. Check if we need to auto-login. // It's an external link, we will open with browser. Check if we need to auto-login.

View File

@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from './app'; import { CoreAppProvider } from './app';
import { CoreInitDelegate } from './init'; import { CoreInitDelegate } from './init';
import { CoreLoggerProvider } from './logger'; import { CoreLoggerProvider } from './logger';
import { CoreSitesProvider } from './sites'; import { CoreSitesProvider, CoreSiteCheckResponse } from './sites';
import { CoreDomUtilsProvider } from './utils/dom'; import { CoreDomUtilsProvider } from './utils/dom';
import { CoreTextUtilsProvider } from './utils/text'; import { CoreTextUtilsProvider } from './utils/text';
import { CoreUrlUtilsProvider } from './utils/url'; import { CoreUrlUtilsProvider } from './utils/url';
@ -44,6 +44,16 @@ export interface CoreCustomURLSchemesParams extends CoreLoginSSOData {
* URL to open once authenticated. * URL to open once authenticated.
*/ */
redirect?: any; redirect?: any;
/**
* Whether it's an SSO token URL.
*/
isSSOToken?: boolean;
/**
* Whether the URL is meant to perform an authentication.
*/
isAuthenticationURL?: boolean;
} }
/* /*
@ -70,21 +80,50 @@ export class CoreCustomURLSchemesProvider {
this.logger = logger.getInstance('CoreCustomURLSchemesProvider'); this.logger = logger.getInstance('CoreCustomURLSchemesProvider');
} }
/**
* Given some data of a custom URL with a token, create a site if it needs to be created.
*
* @param data URL data.
* @return Promise resolved with the site ID.
*/
protected async createSiteIfNeeded(data: CoreCustomURLSchemesParams): Promise<string> {
if (!data.token) {
return;
}
const currentSite = this.sitesProvider.getCurrentSite();
if (!currentSite || currentSite.getToken() != data.token) {
// Token belongs to a different site, create it. It doesn't matter if it already exists.
if (!data.siteUrl.match(/^https?:\/\//)) {
// URL doesn't have a protocol and it's required to be able to create the site. Check which one to use.
const result = await this.sitesProvider.checkSite(data.siteUrl);
data.siteUrl = result.siteUrl;
await this.sitesProvider.checkRequiredMinimumVersion(result.config);
}
return this.sitesProvider.newSite(data.siteUrl, data.token, data.privateToken, data.isSSOToken,
this.loginHelper.getOAuthIdFromParams(data.ssoUrlParams));
} else {
// Token belongs to current site, no need to create it.
return this.sitesProvider.getCurrentSiteId();
}
}
/** /**
* Handle an URL received by custom URL scheme. * Handle an URL received by custom URL scheme.
* *
* @param url URL to treat. * @param url URL to treat.
* @return Promise resolved when done. * @return Promise resolved when done. If rejected, the parameter is of type CoreCustomURLSchemesHandleError.
*/ */
handleCustomURL(url: string): Promise<any> { async handleCustomURL(url: string): Promise<void> {
if (!this.isCustomURL(url)) { if (!this.isCustomURL(url)) {
return Promise.reject(null); throw new CoreCustomURLSchemesHandleError(null);
} }
let modal,
isSSOToken = false,
data: CoreCustomURLSchemesParams;
/* First check that this URL hasn't been treated a few seconds ago. The function that handles custom URL schemes already /* First check that this URL hasn't been treated a few seconds ago. The function that handles custom URL schemes already
does this, but this function is called from other places so we need to handle it in here too. */ does this, but this function is called from other places so we need to handle it in here too. */
if (this.lastUrls[url] && Date.now() - this.lastUrls[url] < 3000) { if (this.lastUrls[url] && Date.now() - this.lastUrls[url] < 3000) {
@ -93,69 +132,49 @@ export class CoreCustomURLSchemesProvider {
} }
this.lastUrls[url] = Date.now(); this.lastUrls[url] = Date.now();
url = this.textUtils.decodeURIComponent(url);
// Wait for app to be ready. // Wait for app to be ready.
return this.initDelegate.ready().then(() => { await this.initDelegate.ready();
url = this.textUtils.decodeURIComponent(url);
// Some platforms like Windows add a slash at the end. Remove it. // 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. // Some sites add a # at the end of the URL. If it's there, remove it.
url = url.replace(/\/?#?\/?$/, ''); url = url.replace(/\/?#?\/?$/, '');
modal = this.domUtils.showModalLoading(); const modal = this.domUtils.showModalLoading();
let data: CoreCustomURLSchemesParams;
// Get the data from the URL. // Get the data from the URL.
try {
if (this.isCustomURLToken(url)) { if (this.isCustomURLToken(url)) {
isSSOToken = true; data = await this.getCustomURLTokenData(url);
return this.getCustomURLTokenData(url);
} else if (this.isCustomURLLink(url)) { } else if (this.isCustomURLLink(url)) {
// In iOS, the protocol after the scheme doesn't have ":". Add it. // In iOS, the protocol after the scheme doesn't have ":". Add it.
url = url.replace(/\/\/link=(https?)\/\//, '//link=$1://'); url = url.replace(/\/\/link=(https?)\/\//, '//link=$1://');
return this.getCustomURLLinkData(url); data = await this.getCustomURLLinkData(url);
} else { } else {
// In iOS, the protocol after the scheme doesn't have ":". Add it. // In iOS, the protocol after the scheme doesn't have ":". Add it.
url = url.replace(/\/\/(https?)\/\//, '//$1://'); url = url.replace(/\/\/(https?)\/\//, '//$1://');
return this.getCustomURLData(url); data = await this.getCustomURLData(url);
} }
}).then((result) => { } catch (error) {
data = result; modal.dismiss();
throw error;
}
try {
if (data.redirect && data.redirect.match(/^https?:\/\//) && data.redirect.indexOf(data.siteUrl) == -1) { if (data.redirect && data.redirect.match(/^https?:\/\//) && data.redirect.indexOf(data.siteUrl) == -1) {
// Redirect URL must belong to the same site. Reject. // Redirect URL must belong to the same site. Reject.
return Promise.reject(this.translate.instant('core.contentlinks.errorredirectothersite')); throw this.translate.instant('core.contentlinks.errorredirectothersite');
} }
// First of all, authenticate the user if needed. // First of all, create the site if needed.
const currentSite = this.sitesProvider.getCurrentSite(); const siteId = await this.createSiteIfNeeded(data);
if (data.token) { if (data.isSSOToken) {
if (!currentSite || currentSite.getToken() != data.token) {
// Token belongs to a different site, create it. It doesn't matter if it already exists.
let promise;
if (!data.siteUrl.match(/^https?:\/\//)) {
// URL doesn't have a protocol and it's required to be able to create the site. Check which one to use.
promise = this.sitesProvider.checkSite(data.siteUrl).then((result) => {
data.siteUrl = result.siteUrl;
});
} else {
promise = Promise.resolve();
}
return promise.then(() => {
return this.sitesProvider.newSite(data.siteUrl, data.token, data.privateToken, isSSOToken,
this.loginHelper.getOAuthIdFromParams(data.ssoUrlParams));
});
} else {
// Token belongs to current site, no need to create it.
return this.sitesProvider.getCurrentSiteId();
}
}
}).then((siteId) => {
if (isSSOToken) {
// Site created and authenticated, open the page to go. // Site created and authenticated, open the page to go.
if (data.pageName) { if (data.pageName) {
// State defined, go to that state instead of site initial page. // State defined, go to that state instead of site initial page.
@ -172,113 +191,58 @@ export class CoreCustomURLSchemesProvider {
data.redirect = this.textUtils.concatenatePaths(data.siteUrl, data.redirect); data.redirect = this.textUtils.concatenatePaths(data.siteUrl, data.redirect);
} }
let promise; let siteIds = [siteId];
if (siteId) { if (!siteId) {
// Site created, we know the site to use. // No site created, check if the site is stored (to know which one to use).
promise = Promise.resolve([siteId]); siteIds = await this.sitesProvider.getSiteIdsFromUrl(data.siteUrl, true, data.username);
} else {
// Check if the site is stored.
promise = this.sitesProvider.getSiteIdsFromUrl(data.siteUrl, true, data.username);
} }
return promise.then((siteIds) => { if (siteIds.length > 1) {
if (siteIds.length > 1) { // More than one site to treat the URL, let the user choose.
// More than one site to treat the URL, let the user choose. this.linksHelper.goToChooseSite(data.redirect || data.siteUrl);
this.linksHelper.goToChooseSite(data.redirect || data.siteUrl);
} else if (siteIds.length == 1) { } else if (siteIds.length == 1) {
// Only one site, handle the link. // Only one site, handle the link.
return this.sitesProvider.getSite(siteIds[0]).then((site) => { const site = await this.sitesProvider.getSite(siteIds[0]);
if (!data.redirect) {
// No redirect, go to the root URL if needed.
return this.linksHelper.handleRootURL(site, false, true);
} else {
// Handle the redirect link.
modal.dismiss(); // Dismiss modal so it doesn't collide with confirms.
/* Always use the username from the site in this case. If the link has a username and a token,
this will make sure that the link is opened with the user the token belongs to. */
const username = site.getInfo().username || data.username;
return this.linksHelper.handleLink(data.redirect, username).then((treated) => {
if (!treated) {
this.domUtils.showErrorModal('core.contentlinks.errornoactions', true);
}
});
}
});
if (!data.redirect) {
// No redirect, go to the root URL if needed.
await this.linksHelper.handleRootURL(site, false, true);
} else { } else {
// Site not stored. Try to add the site. // Handle the redirect link.
return this.sitesProvider.checkSite(data.siteUrl).then((result) => { modal.dismiss(); // Dismiss modal so it doesn't collide with confirms.
// Site exists. We'll allow to add it.
const ssoNeeded = this.loginHelper.isSSOLoginNeeded(result.code),
pageName = 'CoreLoginCredentialsPage',
pageParams = {
siteUrl: result.siteUrl,
username: data.username,
urlToOpen: data.redirect,
siteConfig: result.config
};
let promise,
hasSitePluginsLoaded = false;
modal.dismiss(); // Dismiss modal so it doesn't collide with confirms. /* Always use the username from the site in this case. If the link has a username and a token,
this will make sure that the link is opened with the user the token belongs to. */
const username = site.getInfo().username || data.username;
if (!this.sitesProvider.isLoggedIn()) { const treated = await this.linksHelper.handleLink(data.redirect, username);
// Not logged in, no need to confirm. If SSO the confirm will be shown later.
promise = Promise.resolve();
} else {
// Ask the user before changing site.
const confirmMsg = this.translate.instant('core.contentlinks.confirmurlothersite');
promise = this.domUtils.showConfirm(confirmMsg).then(() => {
if (!ssoNeeded) {
hasSitePluginsLoaded = this.sitePluginsProvider.hasSitePluginsLoaded;
if (hasSitePluginsLoaded) {
// Store the redirect since logout will restart the app.
this.appProvider.storeRedirect(CoreConstants.NO_SITE_ID, pageName, pageParams);
}
return this.sitesProvider.logout().catch(() => { if (!treated) {
// Ignore errors (shouldn't happen). this.domUtils.showErrorModal('core.contentlinks.errornoactions', true);
}); }
}
});
}
return promise.then(() => {
if (ssoNeeded) {
this.loginHelper.confirmAndOpenBrowserForSSOLogin(
result.siteUrl, result.code, result.service, result.config && result.config.launchurl);
} else if (!hasSitePluginsLoaded) {
return this.loginHelper.goToNoSitePage(undefined, pageName, pageParams);
}
});
});
} }
});
}).catch((error) => {
if (error == 'Duplicated') {
// Duplicated request
} else if (error && isSSOToken) {
// An error occurred, display the error and logout the user.
this.loginHelper.treatUserTokenError(data.siteUrl, error);
this.sitesProvider.logout();
} else { } else {
this.domUtils.showErrorModalDefault(error, this.translate.instant('core.login.invalidsite')); // Site not stored. Try to add the site.
const result = await this.sitesProvider.checkSite(data.siteUrl);
// Site exists. We'll allow to add it.
modal.dismiss(); // Dismiss modal so it doesn't collide with confirms.
await this.goToAddSite(data, result);
} }
}).finally(() => {
} catch (error) {
throw new CoreCustomURLSchemesHandleError(error, data);
} finally {
modal.dismiss(); modal.dismiss();
if (isSSOToken) { if (data.isSSOToken) {
this.appProvider.finishSSOAuthentication(); this.appProvider.finishSSOAuthentication();
} }
}); }
} }
/** /**
@ -288,9 +252,9 @@ export class CoreCustomURLSchemesProvider {
* @param url URL to treat. * @param url URL to treat.
* @return Promise resolved with the data. * @return Promise resolved with the data.
*/ */
protected getCustomURLData(url: string): Promise<CoreCustomURLSchemesParams> { protected async getCustomURLData(url: string): Promise<CoreCustomURLSchemesParams> {
if (!this.isCustomURL(url)) { if (!this.isCustomURL(url)) {
return Promise.reject(null); throw new CoreCustomURLSchemesHandleError(null);
} }
// App opened using custom URL scheme. // App opened using custom URL scheme.
@ -313,34 +277,26 @@ export class CoreCustomURLSchemesProvider {
url = url.substr(0, url.indexOf('?')); url = url.substr(0, url.indexOf('?'));
} }
let promise;
if (!url.match(/https?:\/\//)) { if (!url.match(/https?:\/\//)) {
// Url doesn't have a protocol. Check if the site is stored in the app to be able to determine the protocol. // Url doesn't have a protocol. Check if the site is stored in the app to be able to determine the protocol.
promise = this.sitesProvider.getSiteIdsFromUrl(url, true, username).then((siteIds) => { const siteIds = await this.sitesProvider.getSiteIdsFromUrl(url, true, username);
if (siteIds.length) {
// There is at least 1 site with this URL. Use it to know the full URL. if (siteIds.length) {
return this.sitesProvider.getSite(siteIds[0]).then((site) => { // There is at least 1 site with this URL. Use it to know the full URL.
return site.getURL(); const site = await this.sitesProvider.getSite(siteIds[0]);
});
} else { url = site.getURL();
// No site stored with this URL, just use the URL as it is. }
return url;
}
});
} else {
promise = Promise.resolve(url);
} }
return promise.then((url) => { return {
return { siteUrl: url,
siteUrl: url, username: username,
username: username, token: params.token,
token: params.token, privateToken: params.privateToken,
privateToken: params.privateToken, redirect: params.redirect,
redirect: params.redirect isAuthenticationURL: !!params.token,
}; };
});
} }
/** /**
@ -349,9 +305,9 @@ export class CoreCustomURLSchemesProvider {
* @param url URL to treat. * @param url URL to treat.
* @return Promise resolved with the data. * @return Promise resolved with the data.
*/ */
protected getCustomURLLinkData(url: string): Promise<CoreCustomURLSchemesParams> { protected async getCustomURLLinkData(url: string): Promise<CoreCustomURLSchemesParams> {
if (!this.isCustomURLLink(url)) { if (!this.isCustomURLLink(url)) {
return Promise.reject(null); throw new CoreCustomURLSchemesHandleError(null);
} }
// App opened using custom URL scheme. // App opened using custom URL scheme.
@ -367,43 +323,42 @@ export class CoreCustomURLSchemesProvider {
} }
// First of all, check if it's the root URL of a site. // First of all, check if it's the root URL of a site.
return this.sitesProvider.isStoredRootURL(url, username).then((data): any => { const data = await this.sitesProvider.isStoredRootURL(url, username);
if (data.site) { if (data.site) {
// Root URL. // Root URL.
return { return {
siteUrl: data.site.getURL(), siteUrl: data.site.getURL(),
username: username username: username
}; };
} else if (data.siteIds.length > 0) { } else if (data.siteIds.length > 0) {
// Not the root URL, but at least 1 site supports the URL. Get the site URL from the list of sites. // Not the root URL, but at least 1 site supports the URL. Get the site URL from the list of sites.
return this.sitesProvider.getSite(data.siteIds[0]).then((site) => { const site = await this.sitesProvider.getSite(data.siteIds[0]);
return {
siteUrl: site.getURL(),
username: username,
redirect: url
};
});
} else { return {
// Get the site URL. siteUrl: site.getURL(),
let siteUrl = this.linksDelegate.getSiteUrl(url), username: username,
redirect = url; redirect: url
};
if (!siteUrl) { } else {
// Site URL not found, use the original URL since it could be the root URL of the site. // Get the site URL.
siteUrl = url; let siteUrl = this.linksDelegate.getSiteUrl(url);
redirect = undefined; let redirect = url;
}
return { if (!siteUrl) {
siteUrl: siteUrl, // Site URL not found, use the original URL since it could be the root URL of the site.
username: username, siteUrl = url;
redirect: redirect redirect = undefined;
};
} }
});
return {
siteUrl: siteUrl,
username: username,
redirect: redirect
};
}
} }
/** /**
@ -412,14 +367,14 @@ export class CoreCustomURLSchemesProvider {
* @param url URL to treat. * @param url URL to treat.
* @return Promise resolved with the data. * @return Promise resolved with the data.
*/ */
protected getCustomURLTokenData(url: string): Promise<CoreCustomURLSchemesParams> { protected async getCustomURLTokenData(url: string): Promise<CoreCustomURLSchemesParams> {
if (!this.isCustomURLToken(url)) { if (!this.isCustomURLToken(url)) {
return Promise.reject(null); throw new CoreCustomURLSchemesHandleError(null);
} }
if (this.appProvider.isSSOAuthenticationOngoing()) { if (this.appProvider.isSSOAuthenticationOngoing()) {
// Authentication ongoing, probably duplicated request. // Authentication ongoing, probably duplicated request.
return Promise.reject('Duplicated'); throw new CoreCustomURLSchemesHandleError('Duplicated');
} }
if (this.appProvider.isDesktop()) { if (this.appProvider.isDesktop()) {
@ -445,10 +400,56 @@ export class CoreCustomURLSchemesProvider {
// Error decoding the parameter. // Error decoding the parameter.
this.logger.error('Error decoding parameter received for login SSO'); this.logger.error('Error decoding parameter received for login SSO');
return Promise.reject(null); throw new CoreCustomURLSchemesHandleError(null);
} }
return this.loginHelper.validateBrowserSSOLogin(url); const data: CoreCustomURLSchemesParams = await this.loginHelper.validateBrowserSSOLogin(url);
data.isSSOToken = true;
data.isAuthenticationURL = true;
return data;
}
/**
* Go to page to add a site, or open a browser if SSO.
*
* @param data URL data.
* @param checkResponse Result of checkSite.
* @return Promise resolved when done.
*/
protected async goToAddSite(data: CoreCustomURLSchemesParams, checkResponse: CoreSiteCheckResponse): Promise<void> {
const ssoNeeded = this.loginHelper.isSSOLoginNeeded(checkResponse.code);
const pageName = 'CoreLoginCredentialsPage';
const pageParams = {
siteUrl: checkResponse.siteUrl,
username: data.username,
urlToOpen: data.redirect,
siteConfig: checkResponse.config
};
let hasSitePluginsLoaded = false;
if (this.sitesProvider.isLoggedIn()) {
// Ask the user before changing site.
await this.domUtils.showConfirm(this.translate.instant('core.contentlinks.confirmurlothersite'));
if (!ssoNeeded) {
hasSitePluginsLoaded = this.sitePluginsProvider.hasSitePluginsLoaded;
if (hasSitePluginsLoaded) {
// Store the redirect since logout will restart the app.
this.appProvider.storeRedirect(CoreConstants.NO_SITE_ID, pageName, pageParams);
}
await this.sitesProvider.logout();
}
}
if (ssoNeeded) {
this.loginHelper.confirmAndOpenBrowserForSSOLogin(checkResponse.siteUrl, checkResponse.code, checkResponse.service,
checkResponse.config && checkResponse.config.launchurl);
} else if (!hasSitePluginsLoaded) {
await this.loginHelper.goToNoSitePage(undefined, pageName, pageParams);
}
} }
/** /**
@ -522,6 +523,37 @@ export class CoreCustomURLSchemesProvider {
removeCustomURLTokenScheme(url: string): string { removeCustomURLTokenScheme(url: string): string {
return url.replace(CoreConfigConstants.customurlscheme + '://token=', ''); return url.replace(CoreConfigConstants.customurlscheme + '://token=', '');
} }
/**
* Treat error returned by handleCustomURL.
*
* @param error Error data.
*/
treatHandleCustomURLError(error: CoreCustomURLSchemesHandleError): void {
if (error.error == 'Duplicated') {
// Duplicated request
} else if (error.error && error.data && error.data.isSSOToken) {
// An error occurred, display the error and logout the user.
this.loginHelper.treatUserTokenError(error.data.siteUrl, error.error);
this.sitesProvider.logout();
} else {
this.domUtils.showErrorModalDefault(error.error, this.translate.instant('core.login.invalidsite'));
}
}
}
/**
* Error returned by handleCustomURL.
*/
export class CoreCustomURLSchemesHandleError {
/**
* Constructor.
*
* @param error The error message or object.
* @param data Data obtained from the URL (if any).
*/
constructor(public error: any, public data?: CoreCustomURLSchemesParams) { }
} }
export class CoreCustomURLSchemes extends makeSingleton(CoreCustomURLSchemesProvider) {} export class CoreCustomURLSchemes extends makeSingleton(CoreCustomURLSchemesProvider) {}

View File

@ -104,6 +104,33 @@ export class CoreTextUtilsProvider {
return text; return text;
} }
/**
* Add some text to an error message.
*
* @param error Error message or object.
* @param text Text to add.
* @return Modified error.
*/
addTextToError(error: string | CoreTextErrorObject, text: string): string | CoreTextErrorObject {
if (typeof error == 'string') {
return error + text;
}
if (error) {
if (typeof error.message == 'string') {
error.message += text;
} else if (typeof error.error == 'string') {
error.error += text;
} else if (typeof error.content == 'string') {
error.content += text;
} else if (typeof error.body == 'string') {
error.body += text;
}
}
return error;
}
/** /**
* Given an address as a string, return a URL to open the address in maps. * Given an address as a string, return a URL to open the address in maps.
* *