Vmeda.Online/src/core/services/urlschemes.ts

563 lines
20 KiB
TypeScript
Raw Normal View History

// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreError } from '@classes/errors/error';
import { CoreWSError } from '@classes/errors/wserror';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
import { CoreLoginHelper, CoreLoginSSOData } from '@features/login/services/login-helper';
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
import { ApplicationInit, makeSingleton, Translate } from '@singletons';
import { CoreLogger } from '@singletons/logger';
import { CoreConstants } from '../constants';
import { CoreApp } from './app';
import { CoreNavigator } from './navigator';
import { CoreSiteCheckResponse, CoreSites } from './sites';
import { CoreDomUtils } from './utils/dom';
import { CoreTextErrorObject, CoreTextUtils } from './utils/text';
import { CoreUrlUtils } from './utils/url';
import { CoreUtils } from './utils/utils';
/*
* Provider to handle custom URL schemes.
*/
@Injectable({ providedIn: 'root' })
export class CoreCustomURLSchemesProvider {
protected logger: CoreLogger;
protected lastUrls: Record<string, number> = {};
constructor() {
this.logger = CoreLogger.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 if created or already exists.
*/
protected async createSiteIfNeeded(data: CoreCustomURLSchemesParams): Promise<string | undefined> {
if (!data.token) {
return;
}
const currentSite = CoreSites.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 CoreSites.checkSite(data.siteUrl);
data.siteUrl = result.siteUrl;
await CoreSites.checkApplication(result);
}
return CoreSites.newSite(
data.siteUrl,
data.token,
data.privateToken,
!!data.isSSOToken,
CoreLoginHelper.getOAuthIdFromParams(data.ssoUrlParams),
);
} else {
// Token belongs to current site, no need to create it.
return CoreSites.getCurrentSiteId();
}
}
/**
* Handle an URL received by custom URL scheme.
*
* @param url URL to treat.
* @return Promise resolved when done. If rejected, the parameter is of type CoreCustomURLSchemesHandleError.
*/
async handleCustomURL(url: string): Promise<void> {
if (!this.isCustomURL(url)) {
throw new CoreCustomURLSchemesHandleError(null);
}
/* 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) {
// Function called more than once, stop.
return;
}
this.lastUrls[url] = Date.now();
url = CoreTextUtils.decodeURIComponent(url);
// Wait for app to be ready.
await ApplicationInit.donePromise;
// 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(/\/?#?\/?$/, '');
const modal = await CoreDomUtils.showModalLoading();
let data: CoreCustomURLSchemesParams;
// Get the data from the URL.
try {
if (this.isCustomURLToken(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://');
data = await this.getCustomURLLinkData(url);
} else {
// In iOS, the protocol after the scheme doesn't have ":". Add it.
url = url.replace(/\/\/(https?)\/\//, '//$1://');
data = await this.getCustomURLData(url);
}
} catch (error) {
modal.dismiss();
throw error;
}
try {
const isValid = await CoreLoginHelper.isSiteUrlAllowed(data.siteUrl);
if (!isValid) {
throw Translate.instant('core.errorurlschemeinvalidsite');
}
if (data.redirect && data.redirect.match(/^https?:\/\//) && data.redirect.indexOf(data.siteUrl) == -1) {
// Redirect URL must belong to the same site. Reject.
throw Translate.instant('core.contentlinks.errorredirectothersite');
}
// First of all, create the site if needed.
const siteId = await this.createSiteIfNeeded(data);
if (data.isSSOToken || (data.isAuthenticationURL && siteId && CoreSites.getCurrentSiteId() == siteId)) {
// Site created and authenticated, open the page to go.
if (data.pageName) {
// Page defined, go to that page instead of site initial page.
CoreNavigator.navigateToSitePath(data.pageName, data.pageOptions);
} else {
CoreNavigator.navigateToSiteHome();
}
return;
}
if (data.redirect && !data.redirect.match(/^https?:\/\//)) {
// Redirect is a relative URL. Append the site URL.
data.redirect = CoreTextUtils.concatenatePaths(data.siteUrl, data.redirect);
}
let siteIds = [siteId];
if (!siteId) {
// No site created, check if the site is stored (to know which one to use).
siteIds = await CoreSites.getSiteIdsFromUrl(data.siteUrl, true, data.username);
}
if (siteIds.length > 1) {
// More than one site to treat the URL, let the user choose.
CoreContentLinksHelper.goToChooseSite(data.redirect || data.siteUrl);
} else if (siteIds.length == 1) {
// Only one site, handle the link.
const site = await CoreSites.getSite(siteIds[0]);
if (!data.redirect) {
// No redirect, go to the root URL if needed.
await CoreContentLinksHelper.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;
const treated = await CoreContentLinksHelper.handleLink(data.redirect, username);
if (!treated) {
CoreDomUtils.showErrorModal('core.contentlinks.errornoactions', true);
}
}
} else {
// Site not stored. Try to add the site.
const result = await CoreSites.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);
}
} catch (error) {
throw new CoreCustomURLSchemesHandleError(error, data);
} finally {
modal.dismiss();
if (data.isSSOToken) {
CoreApp.finishSSOAuthentication();
}
}
}
/**
* Get the data from a custom URL scheme. The structure of the URL is:
* moodlemobile://username@domain.com?token=TOKEN&privatetoken=PRIVATETOKEN&redirect=http://domain.com/course/view.php?id=2
*
* @param url URL to treat.
* @return Promise resolved with the data.
*/
protected async getCustomURLData(url: string): Promise<CoreCustomURLSchemesParams> {
if (!this.isCustomURL(url)) {
throw new CoreCustomURLSchemesHandleError(null);
}
// App opened using custom URL scheme.
this.logger.debug('Treating custom URL scheme: ' + url);
// Delete the sso scheme from the URL.
url = this.removeCustomURLScheme(url);
// Detect if there's a user specified.
const username = CoreUrlUtils.getUsernameFromUrl(url);
if (username) {
url = url.replace(username + '@', ''); // Remove the username from the URL.
}
// Get the params of the URL.
const params = CoreUrlUtils.extractUrlParams(url);
// Remove the params to get the site URL.
if (url.indexOf('?') != -1) {
url = url.substr(0, url.indexOf('?'));
}
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.
const siteIds = await CoreSites.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 CoreSites.getSite(siteIds[0]);
url = site.getURL();
}
}
return {
siteUrl: url,
username: username,
token: params.token,
privateToken: params.privateToken,
redirect: params.redirect,
isAuthenticationURL: !!params.token,
};
}
/**
* Get the data from a "link" custom URL scheme. This kind of URL is deprecated.
*
* @param url URL to treat.
* @return Promise resolved with the data.
*/
protected async getCustomURLLinkData(url: string): Promise<CoreCustomURLSchemesParams> {
if (!this.isCustomURLLink(url)) {
throw new CoreCustomURLSchemesHandleError(null);
}
// App opened using custom URL scheme.
this.logger.debug('Treating custom URL scheme with link param: ' + url);
// Delete the sso scheme from the URL.
url = this.removeCustomURLLinkScheme(url);
// Detect if there's a user specified.
const username = CoreUrlUtils.getUsernameFromUrl(url);
if (username) {
url = url.replace(username + '@', ''); // Remove the username from the URL.
}
// First of all, check if it's the root URL of a site.
const data = await CoreSites.isStoredRootURL(url, 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.
const site = await CoreSites.getSite(data.siteIds[0]);
return {
siteUrl: site.getURL(),
username: username,
redirect: url,
};
} else {
// Get the site URL.
let siteUrl = CoreContentLinksDelegate.getSiteUrl(url);
let redirect: string | undefined = 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;
}
return {
siteUrl: siteUrl,
username: username,
redirect: redirect,
};
}
}
/**
* Get the data from a "token" custom URL scheme. This kind of URL is deprecated.
*
* @param url URL to treat.
* @return Promise resolved with the data.
*/
protected async getCustomURLTokenData(url: string): Promise<CoreCustomURLSchemesParams> {
if (!this.isCustomURLToken(url)) {
throw new CoreCustomURLSchemesHandleError(null);
}
if (CoreApp.isSSOAuthenticationOngoing()) {
// Authentication ongoing, probably duplicated request.
throw new CoreCustomURLSchemesHandleError('Duplicated');
}
// App opened using custom URL scheme. Probably an SSO authentication.
CoreApp.startSSOAuthentication();
this.logger.debug('App launched by URL with an SSO');
// Delete the sso scheme from the URL.
url = this.removeCustomURLTokenScheme(url);
// 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');
throw new CoreCustomURLSchemesHandleError(null);
}
const data: CoreCustomURLSchemesParams = await CoreLoginHelper.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 = CoreLoginHelper.isSSOLoginNeeded(checkResponse.code);
const pageParams = {
siteUrl: checkResponse.siteUrl,
username: data.username,
urlToOpen: data.redirect,
siteConfig: checkResponse.config,
};
let hasSitePluginsLoaded = false;
if (CoreSites.isLoggedIn()) {
// Ask the user before changing site.
await CoreDomUtils.showConfirm(Translate.instant('core.contentlinks.confirmurlothersite'));
if (!ssoNeeded) {
hasSitePluginsLoaded = CoreSitePlugins.hasSitePluginsLoaded;
if (hasSitePluginsLoaded) {
// Store the redirect since logout will restart the app.
CoreApp.storeRedirect(CoreConstants.NO_SITE_ID, '/login/credentials', { params: pageParams });
}
await CoreSites.logout();
}
}
if (ssoNeeded) {
CoreLoginHelper.confirmAndOpenBrowserForSSOLogin(
checkResponse.siteUrl,
checkResponse.code,
checkResponse.service,
checkResponse.config?.launchurl,
);
} else if (!hasSitePluginsLoaded) {
await CoreNavigator.navigateToLoginCredentials(pageParams);
}
}
/**
* Check whether a URL is a custom URL scheme.
*
* @param url URL to check.
* @return Whether it's a custom URL scheme.
*/
isCustomURL(url: string): boolean {
if (!url) {
return false;
}
return url.indexOf(CoreConstants.CONFIG.customurlscheme + '://') != -1;
}
/**
* Check whether a URL is a custom URL scheme with the "link" param (deprecated).
*
* @param url URL to check.
* @return Whether it's a custom URL scheme.
*/
isCustomURLLink(url: string): boolean {
if (!url) {
return false;
}
return url.indexOf(CoreConstants.CONFIG.customurlscheme + '://link=') != -1;
}
/**
* Check whether a URL is a custom URL scheme with a "token" param (deprecated).
*
* @param url URL to check.
* @return Whether it's a custom URL scheme.
*/
isCustomURLToken(url: string): boolean {
if (!url) {
return false;
}
return url.indexOf(CoreConstants.CONFIG.customurlscheme + '://token=') != -1;
}
/**
* Remove the scheme from a custom URL.
*
* @param url URL to treat.
* @return URL without scheme.
*/
removeCustomURLScheme(url: string): string {
return url.replace(CoreConstants.CONFIG.customurlscheme + '://', '');
}
/**
* Remove the scheme and the "link=" prefix from a link custom URL.
*
* @param url URL to treat.
* @return URL without scheme and prefix.
*/
removeCustomURLLinkScheme(url: string): string {
return url.replace(CoreConstants.CONFIG.customurlscheme + '://link=', '');
}
/**
* Remove the scheme and the "token=" prefix from a token custom URL.
*
* @param url URL to treat.
* @return URL without scheme and prefix.
*/
removeCustomURLTokenScheme(url: string): string {
return url.replace(CoreConstants.CONFIG.customurlscheme + '://token=', '');
}
/**
* Treat error returned by handleCustomURL.
*
* @param error Error data.
*/
treatHandleCustomURLError(error: CoreCustomURLSchemesHandleError): void {
if (error.error == 'Duplicated') {
// Duplicated request
} else if (CoreUtils.isWebServiceError(error.error) && error.data && error.data.isSSOToken) {
// An error occurred, display the error and logout the user.
CoreLoginHelper.treatUserTokenError(error.data.siteUrl, <CoreWSError> error.error);
CoreSites.logout();
} else {
CoreDomUtils.showErrorModalDefault(error.error, Translate.instant('core.login.invalidsite'));
}
}
}
/**
* Error returned by handleCustomURL.
*/
export class CoreCustomURLSchemesHandleError extends CoreError {
/**
* Constructor.
*
* @param error The error message or object.
* @param data Data obtained from the URL (if any).
*/
constructor(public error: string | CoreError | CoreTextErrorObject | null, public data?: CoreCustomURLSchemesParams) {
super(CoreTextUtils.getErrorMessageFromError(error));
}
}
export const CoreCustomURLSchemes = makeSingleton(CoreCustomURLSchemesProvider);
/**
* All params that can be in a custom URL scheme.
*/
export interface CoreCustomURLSchemesParams extends CoreLoginSSOData {
/**
* Username.
*/
username?: string;
/**
* URL to open once authenticated.
*/
redirect?: string;
/**
* Whether it's an SSO token URL.
*/
isSSOToken?: boolean;
/**
* Whether the URL is meant to perform an authentication.
*/
isAuthenticationURL?: boolean;
}