diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 8316ea5fb..54d49f357 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -22,7 +22,7 @@ import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; 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 { Keyboard } from '@ionic-native/keyboard'; import { ScreenOrientation } from '@ionic-native/screen-orientation'; @@ -140,7 +140,9 @@ export class MoodleMobileApp implements OnInit { if (this.urlSchemesProvider.isCustomURL(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); } else if (this.platform.is('android')) { @@ -194,7 +196,9 @@ export class MoodleMobileApp implements OnInit { this.lastUrls[url] = Date.now(); this.eventsProvider.trigger(CoreEventsProvider.APP_LAUNCHED_URL, url); - this.urlSchemesProvider.handleCustomURL(url); + this.urlSchemesProvider.handleCustomURL(url).catch((error: CoreCustomURLSchemesHandleError) => { + this.urlSchemesProvider.treatHandleCustomURLError(error); + }); }); }; diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 193426e84..e939777b9 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1789,6 +1789,7 @@ "core.login.usernotaddederror": "User not added - error", "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.youcanstillconnectwithcredentials": "You can still connect to the site by entering your username and password.", "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.mainmenu.changesite": "Change site", diff --git a/src/core/login/lang/en.json b/src/core/login/lang/en.json index 7631d77bc..fae8f54da 100644 --- a/src/core/login/lang/en.json +++ b/src/core/login/lang/en.json @@ -100,6 +100,7 @@ "usernamerequired": "Username required", "usernotaddederror": "User not added - error", "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" } \ No newline at end of file diff --git a/src/core/login/pages/site/site.ts b/src/core/login/pages/site/site.ts index ddf2dcc7c..474c9f523 100644 --- a/src/core/login/pages/site/site.ts +++ b/src/core/login/pages/site/site.ts @@ -17,10 +17,11 @@ import { IonicPage, NavController, ModalController, AlertController, NavParams } import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; 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 { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreConfigConstants } from '../../../../configconstants'; import { CoreLoginHelperProvider } from '../../providers/helper'; import { FormBuilder, FormGroup, ValidatorFn, AbstractControl } from '@angular/forms'; @@ -74,7 +75,8 @@ export class CoreLoginSitePage { protected eventsProvider: CoreEventsProvider, protected translate: TranslateService, protected utils: CoreUtilsProvider, - private urlSchemesProvider: CoreCustomURLSchemesProvider) { + protected urlSchemesProvider: CoreCustomURLSchemesProvider, + protected textUtils: CoreTextUtilsProvider) { this.showKeyboard = !!navParams.get('showKeyboard'); this.showScanQR = this.utils.canScanQR(); @@ -363,19 +365,69 @@ export class CoreLoginSitePage { /** * Scan a QR code and put its text in the URL input. */ - scanQR(): void { + async scanQR(): Promise { // Scan for a QR code. - this.utils.scanQR().then((text) => { - 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); + const text = await this.utils.scanQR(); - 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 { + 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, + '

' + this.translate.instant('core.login.youcanstillconnectwithcredentials')); + + this.urlSchemesProvider.treatHandleCustomURLError(error); } } diff --git a/src/core/mainmenu/pages/more/more.ts b/src/core/mainmenu/pages/more/more.ts index bf2fb13af..f583edc2d 100644 --- a/src/core/mainmenu/pages/more/more.ts +++ b/src/core/mainmenu/pages/more/more.ts @@ -16,7 +16,7 @@ import { Component, OnDestroy } from '@angular/core'; import { IonicPage, NavController } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; -import { CoreCustomURLSchemesProvider } from '@providers/urlschemes'; +import { CoreCustomURLSchemesProvider, CoreCustomURLSchemesHandleError } from '@providers/urlschemes'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../providers/delegate'; @@ -179,7 +179,9 @@ export class CoreMainMenuMorePage implements OnDestroy { if (text) { if (this.urlSchemesProvider.isCustomURL(text)) { // 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. // Check if the app can handle the URL. this.linkHelper.handleLink(text, undefined, this.navCtrl, true, true).then((treated) => { diff --git a/src/directives/link.ts b/src/directives/link.ts index 9643f08ac..2a18a11e5 100644 --- a/src/directives/link.ts +++ b/src/directives/link.ts @@ -21,7 +21,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; 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. @@ -113,7 +113,9 @@ export class CoreLinkDirective implements OnInit { this.domUtils.scrollToElementBySelector(this.content, '#' + href + ', [name=\'' + href + '\']'); } } else if (this.urlSchemesProvider.isCustomURL(href)) { - this.urlSchemesProvider.handleCustomURL(href); + this.urlSchemesProvider.handleCustomURL(href).catch((error: CoreCustomURLSchemesHandleError) => { + this.urlSchemesProvider.treatHandleCustomURLError(error); + }); } else { // It's an external link, we will open with browser. Check if we need to auto-login. diff --git a/src/providers/urlschemes.ts b/src/providers/urlschemes.ts index d2a2f9526..1ce94172c 100644 --- a/src/providers/urlschemes.ts +++ b/src/providers/urlschemes.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from './app'; import { CoreInitDelegate } from './init'; import { CoreLoggerProvider } from './logger'; -import { CoreSitesProvider } from './sites'; +import { CoreSitesProvider, CoreSiteCheckResponse } from './sites'; import { CoreDomUtilsProvider } from './utils/dom'; import { CoreTextUtilsProvider } from './utils/text'; import { CoreUrlUtilsProvider } from './utils/url'; @@ -44,6 +44,16 @@ export interface CoreCustomURLSchemesParams extends CoreLoginSSOData { * URL to open once authenticated. */ 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'); } + /** + * 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 { + 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. * * @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 { + async handleCustomURL(url: string): Promise { 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 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) { @@ -93,69 +132,49 @@ export class CoreCustomURLSchemesProvider { } this.lastUrls[url] = Date.now(); + url = this.textUtils.decodeURIComponent(url); // Wait for app to be ready. - return this.initDelegate.ready().then(() => { - url = this.textUtils.decodeURIComponent(url); + await this.initDelegate.ready(); - // 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(/\/?#?\/?$/, ''); + // 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(/\/?#?\/?$/, ''); - 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)) { - isSSOToken = true; - - return this.getCustomURLTokenData(url); + data = await this.getCustomURLTokenData(url); } else if (this.isCustomURLLink(url)) { // In iOS, the protocol after the scheme doesn't have ":". Add it. url = url.replace(/\/\/link=(https?)\/\//, '//link=$1://'); - return this.getCustomURLLinkData(url); + data = await this.getCustomURLLinkData(url); } else { // In iOS, the protocol after the scheme doesn't have ":". Add it. url = url.replace(/\/\/(https?)\/\//, '//$1://'); - return this.getCustomURLData(url); + data = await this.getCustomURLData(url); } - }).then((result) => { - data = result; + } catch (error) { + modal.dismiss(); + throw error; + } + + try { if (data.redirect && data.redirect.match(/^https?:\/\//) && data.redirect.indexOf(data.siteUrl) == -1) { // 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. - const currentSite = this.sitesProvider.getCurrentSite(); + // First of all, create the site if needed. + const siteId = await this.createSiteIfNeeded(data); - if (data.token) { - 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) { + if (data.isSSOToken) { // Site created and authenticated, open the page to go. if (data.pageName) { // 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); } - let promise; + let siteIds = [siteId]; - if (siteId) { - // Site created, we know the site to use. - promise = Promise.resolve([siteId]); - } else { - // Check if the site is stored. - promise = this.sitesProvider.getSiteIdsFromUrl(data.siteUrl, true, data.username); + if (!siteId) { + // No site created, check if the site is stored (to know which one to use). + siteIds = await this.sitesProvider.getSiteIdsFromUrl(data.siteUrl, true, data.username); } - return promise.then((siteIds) => { - if (siteIds.length > 1) { - // More than one site to treat the URL, let the user choose. - this.linksHelper.goToChooseSite(data.redirect || data.siteUrl); + if (siteIds.length > 1) { + // More than one site to treat the URL, let the user choose. + this.linksHelper.goToChooseSite(data.redirect || data.siteUrl); - } else if (siteIds.length == 1) { - // Only one site, handle the link. - return this.sitesProvider.getSite(siteIds[0]).then((site) => { - 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); - } - }); - } - }); + } else if (siteIds.length == 1) { + // Only one site, handle the link. + const site = await this.sitesProvider.getSite(siteIds[0]); + if (!data.redirect) { + // No redirect, go to the root URL if needed. + await this.linksHelper.handleRootURL(site, false, true); } else { - // Site not stored. Try to add the site. - return this.sitesProvider.checkSite(data.siteUrl).then((result) => { - // 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; + // Handle the redirect link. + modal.dismiss(); // Dismiss modal so it doesn't collide with confirms. - 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()) { - // 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); - } + const treated = await this.linksHelper.handleLink(data.redirect, username); - return this.sitesProvider.logout().catch(() => { - // Ignore errors (shouldn't happen). - }); - } - }); - } - - 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); - } - }); - - }); + if (!treated) { + this.domUtils.showErrorModal('core.contentlinks.errornoactions', true); + } } - }); - }).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 { - 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(); - if (isSSOToken) { + if (data.isSSOToken) { this.appProvider.finishSSOAuthentication(); } - }); - + } } /** @@ -288,9 +252,9 @@ export class CoreCustomURLSchemesProvider { * @param url URL to treat. * @return Promise resolved with the data. */ - protected getCustomURLData(url: string): Promise { + protected async getCustomURLData(url: string): Promise { if (!this.isCustomURL(url)) { - return Promise.reject(null); + throw new CoreCustomURLSchemesHandleError(null); } // App opened using custom URL scheme. @@ -313,34 +277,26 @@ export class CoreCustomURLSchemesProvider { url = url.substr(0, url.indexOf('?')); } - let promise; - 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. - promise = this.sitesProvider.getSiteIdsFromUrl(url, true, username).then((siteIds) => { - if (siteIds.length) { - // There is at least 1 site with this URL. Use it to know the full URL. - return this.sitesProvider.getSite(siteIds[0]).then((site) => { - return site.getURL(); - }); - } else { - // No site stored with this URL, just use the URL as it is. - return url; - } - }); - } else { - promise = Promise.resolve(url); + 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. + const site = await this.sitesProvider.getSite(siteIds[0]); + + url = site.getURL(); + } } - return promise.then((url) => { - return { - siteUrl: url, - username: username, - token: params.token, - privateToken: params.privateToken, - redirect: params.redirect - }; - }); + return { + siteUrl: url, + username: username, + token: params.token, + privateToken: params.privateToken, + redirect: params.redirect, + isAuthenticationURL: !!params.token, + }; } /** @@ -349,9 +305,9 @@ export class CoreCustomURLSchemesProvider { * @param url URL to treat. * @return Promise resolved with the data. */ - protected getCustomURLLinkData(url: string): Promise { + protected async getCustomURLLinkData(url: string): Promise { if (!this.isCustomURLLink(url)) { - return Promise.reject(null); + throw new CoreCustomURLSchemesHandleError(null); } // 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. - return this.sitesProvider.isStoredRootURL(url, username).then((data): any => { + const data = await this.sitesProvider.isStoredRootURL(url, username); - if (data.site) { - // Root URL. - return { - siteUrl: data.site.getURL(), - username: username - }; + if (data.site) { + // Root URL. + return { + siteUrl: data.site.getURL(), + username: username + }; - } 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. - return this.sitesProvider.getSite(data.siteIds[0]).then((site) => { - return { - siteUrl: site.getURL(), - username: username, - redirect: url - }; - }); + } 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. + const site = await this.sitesProvider.getSite(data.siteIds[0]); - } else { - // Get the site URL. - let siteUrl = this.linksDelegate.getSiteUrl(url), - redirect = url; + return { + siteUrl: site.getURL(), + username: username, + redirect: url + }; - if (!siteUrl) { - // Site URL not found, use the original URL since it could be the root URL of the site. - siteUrl = url; - redirect = undefined; - } + } else { + // Get the site URL. + let siteUrl = this.linksDelegate.getSiteUrl(url); + let redirect = url; - return { - siteUrl: siteUrl, - username: username, - redirect: redirect - }; + if (!siteUrl) { + // Site URL not found, use the original URL since it could be the root URL of the site. + siteUrl = url; + redirect = undefined; } - }); + + return { + siteUrl: siteUrl, + username: username, + redirect: redirect + }; + } } /** @@ -412,14 +367,14 @@ export class CoreCustomURLSchemesProvider { * @param url URL to treat. * @return Promise resolved with the data. */ - protected getCustomURLTokenData(url: string): Promise { + protected async getCustomURLTokenData(url: string): Promise { if (!this.isCustomURLToken(url)) { - return Promise.reject(null); + throw new CoreCustomURLSchemesHandleError(null); } if (this.appProvider.isSSOAuthenticationOngoing()) { // Authentication ongoing, probably duplicated request. - return Promise.reject('Duplicated'); + throw new CoreCustomURLSchemesHandleError('Duplicated'); } if (this.appProvider.isDesktop()) { @@ -445,10 +400,56 @@ export class CoreCustomURLSchemesProvider { // Error decoding the parameter. 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 { + 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 { 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) {} diff --git a/src/providers/utils/text.ts b/src/providers/utils/text.ts index 66e617091..3cac7f846 100644 --- a/src/providers/utils/text.ts +++ b/src/providers/utils/text.ts @@ -104,6 +104,33 @@ export class CoreTextUtilsProvider { 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. *