From a476a773f10264734ce1735d9690c3668f5a6de4 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 23 Jan 2020 10:33:43 +0100 Subject: [PATCH] MOBILE-3286 login: Improve guessing site domains --- src/classes/utils/url.ts | 61 ++++++++++++++++++++++++------- src/core/login/pages/site/site.ts | 16 +++----- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/src/classes/utils/url.ts b/src/classes/utils/url.ts index 579d8cbb0..2539d19c6 100644 --- a/src/classes/utils/url.ts +++ b/src/classes/utils/url.ts @@ -27,6 +27,11 @@ interface UrlParts { */ domain?: string; + /** + * Url port. + */ + port?: string; + /** * Url path. */ @@ -49,39 +54,69 @@ interface UrlParts { */ export class CoreUrl { - // Avoid creating singleton instances + // Avoid creating singleton instances. private constructor() {} /** * Parse parts of a url, using an implicit protocol if it is missing from the url. * * @param url Url. - * @param implicitProtocol Protocol to be used if the url doesn't have any. * @return Url parts. */ - static parse(url: string, implicitProtocol?: string): UrlParts | null { - // Prepare url before parsing - url = url.trim(); - - if (implicitProtocol && !url.match(/^[a-zA-Z]+:\/\//)) { - url = `${implicitProtocol}://${url}`; - } - - // Regular expression taken from RFC 3986: https://tools.ietf.org/html/rfc3986#appendix-B + static parse(url: string): UrlParts | null { + // Parse url with regular expression taken from RFC 3986: https://tools.ietf.org/html/rfc3986#appendix-B. const match = url.trim().match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/); if (!match) { return null; } - // Prepare parts replacing empty strings with undefined + // Split host into domain and port. + const host = match[4] || ''; + const [domain, port]: string[] = host.indexOf(':') === -1 ? [host] : host.split(':'); + + // Prepare parts replacing empty strings with undefined. return { protocol: match[2] || undefined, - domain: match[4] || undefined, + domain: domain || undefined, + port: port || undefined, path: match[5] || undefined, query: match[7] || undefined, fragment: match[9] || undefined, }; } + /** + * Guess the Moodle domain from a site url. + * + * @param url Site url. + * @return Guessed Moodle domain. + */ + static guessMoodleDomain(url: string): string | null { + // Add protocol if it was missing. Moodle can only be served through http or https, so this is a fair assumption to make. + if (!url.match(/^https?:\/\//)) { + url = `https://${url}`; + } + + // Match using common suffixes. + const knownSuffixes = [ + '\/my\/?', + '\/\\\?redirect=0', + '\/index\\\.php', + '\/course\/view\\\.php', + '\/login\/index\\\.php', + '\/mod\/page\/view\\\.php', + ]; + const match = url.match(new RegExp(`^https?:\/\/(.*?)(${knownSuffixes.join('|')})`)); + + if (match) { + return match[1]; + } + + // If nothing else worked, parse the domain. + const urlParts = CoreUrl.parse(url); + + return urlParts && urlParts.domain ? urlParts.domain : null; + } + } diff --git a/src/core/login/pages/site/site.ts b/src/core/login/pages/site/site.ts index 27db57687..5a231d27c 100644 --- a/src/core/login/pages/site/site.ts +++ b/src/core/login/pages/site/site.ts @@ -87,6 +87,8 @@ export class CoreLoginSitePage { return; } + url = url.trim(); + const modal = this.domUtils.showModalLoading(), siteData = this.sitesProvider.getDemoSiteData(url); @@ -113,18 +115,12 @@ export class CoreLoginSitePage { } else { // Not a demo site. this.sitesProvider.checkSite(url) - - // Attempt parsing the domain after initial check failed .catch((error) => { - const urlParts = CoreUrl.parse(url, 'http'); + // Attempt guessing the domain if the initial check failed + const domain = CoreUrl.guessMoodleDomain(url); - if (!urlParts || !urlParts.domain) { - throw error; - } - - return this.sitesProvider.checkSite(urlParts.domain); + return domain ? this.sitesProvider.checkSite(domain) : Promise.reject(error); }) - .then((result) => this.login(result)) .catch((error) => this.showLoginIssue(url, error)) .finally(() => modal.dismiss()); @@ -177,7 +173,7 @@ export class CoreLoginSitePage { * * @return Promise resolved after logging in. */ - private async login(response: CoreSiteCheckResponse): Promise { + protected async login(response: CoreSiteCheckResponse): Promise { return this.sitesProvider.checkRequiredMinimumVersion(response.config).then(() => { if (response.warning) { this.domUtils.showErrorModal(response.warning, true, 4000);