From 28404f8b25aa9daf517f95cce1003edb547b7e22 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 16 Jan 2020 15:43:11 +0100 Subject: [PATCH 1/3] MOBILE-3286 login: Parse url domain if site wasn't found --- src/classes/utils/url.ts | 29 ++++++++++++++++ src/core/login/pages/site/site.ts | 55 ++++++++++++++++++++----------- 2 files changed, 64 insertions(+), 20 deletions(-) create mode 100644 src/classes/utils/url.ts diff --git a/src/classes/utils/url.ts b/src/classes/utils/url.ts new file mode 100644 index 000000000..1fb990b81 --- /dev/null +++ b/src/classes/utils/url.ts @@ -0,0 +1,29 @@ +// (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. + +export class CoreUrl { + + /** + * Parse url domain. + * + * @param url Url. + * @return Url domain. + */ + static parseDomain(url: string): string | null { + const match = url.trim().match(/(https?:\/\/|^)([^/]+)/); + + return match ? match[2] : null; + } + +} diff --git a/src/core/login/pages/site/site.ts b/src/core/login/pages/site/site.ts index f9bc3a139..b1b7dbd9b 100644 --- a/src/core/login/pages/site/site.ts +++ b/src/core/login/pages/site/site.ts @@ -15,11 +15,12 @@ import { Component } from '@angular/core'; import { IonicPage, NavController, ModalController, NavParams } from 'ionic-angular'; import { CoreAppProvider } from '@providers/app'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSiteCheckResponse } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreConfigConstants } from '../../../../configconstants'; import { CoreLoginHelperProvider } from '../../providers/helper'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { CoreUrl } from '@classes/utils/url'; /** * Page to enter or select the site URL to connect to. @@ -111,27 +112,22 @@ export class CoreLoginSitePage { } else { // Not a demo site. - this.sitesProvider.checkSite(url).then((result) => { - return this.sitesProvider.checkRequiredMinimumVersion(result.config).then(() => { - if (result.warning) { - this.domUtils.showErrorModal(result.warning, true, 4000); + this.sitesProvider.checkSite(url) + + // Attempt parsing the domain after initial check failed + .catch((error) => { + const domain = CoreUrl.parseDomain(url); + + if (!domain) { + throw error; } - if (this.loginHelper.isSSOLoginNeeded(result.code)) { - // SSO. User needs to authenticate in a browser. - this.loginHelper.confirmAndOpenBrowserForSSOLogin( - result.siteUrl, result.code, result.service, result.config && result.config.launchurl); - } else { - this.navCtrl.push('CoreLoginCredentialsPage', { siteUrl: result.siteUrl, siteConfig: result.config }); - } - }).catch(() => { - // Ignore errors. - }); - }, (error) => { - this.showLoginIssue(url, error); - }).finally(() => { - modal.dismiss(); - }); + return this.sitesProvider.checkSite(domain); + }) + + .then((result) => this.login(result)) + .catch((error) => this.showLoginIssue(url, error)) + .finally(() => modal.dismiss()); } } @@ -173,4 +169,23 @@ export class CoreLoginSitePage { modal.present(); } + + private async login(response: CoreSiteCheckResponse): Promise { + return this.sitesProvider.checkRequiredMinimumVersion(response.config).then(() => { + if (response.warning) { + this.domUtils.showErrorModal(response.warning, true, 4000); + } + + if (this.loginHelper.isSSOLoginNeeded(response.code)) { + // SSO. User needs to authenticate in a browser. + this.loginHelper.confirmAndOpenBrowserForSSOLogin( + response.siteUrl, response.code, response.service, response.config && response.config.launchurl); + } else { + this.navCtrl.push('CoreLoginCredentialsPage', { siteUrl: response.siteUrl, siteConfig: response.config }); + } + }).catch(() => { + // Ignore errors. + }); + } + } From 2e69497c130bde3626fd65f2c70a628c6c67ae33 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 21 Jan 2020 10:37:42 +0100 Subject: [PATCH 2/3] MOBILE-3286 utils: Refactor CoreUrl and expose to plugins --- src/classes/utils/url.ts | 70 ++++++++++++++++++++++++--- src/core/compile/providers/compile.ts | 2 + src/core/login/pages/site/site.ts | 13 +++-- 3 files changed, 76 insertions(+), 9 deletions(-) diff --git a/src/classes/utils/url.ts b/src/classes/utils/url.ts index 1fb990b81..579d8cbb0 100644 --- a/src/classes/utils/url.ts +++ b/src/classes/utils/url.ts @@ -12,18 +12,76 @@ // See the License for the specific language governing permissions and // limitations under the License. -export class CoreUrl { +/** + * Parts contained within a url. + */ +interface UrlParts { /** - * Parse url domain. + * Url protocol. + */ + protocol?: string; + + /** + * Url domain. + */ + domain?: string; + + /** + * Url path. + */ + path?: string; + + /** + * Url query. + */ + query?: string; + + /** + * Url fragment. + */ + fragment?: string; + +} + +/** + * Singleton with helper functions for urls. + */ +export class CoreUrl { + + // 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. - * @return Url domain. + * @param implicitProtocol Protocol to be used if the url doesn't have any. + * @return Url parts. */ - static parseDomain(url: string): string | null { - const match = url.trim().match(/(https?:\/\/|^)([^/]+)/); + static parse(url: string, implicitProtocol?: string): UrlParts | null { + // Prepare url before parsing + url = url.trim(); - return match ? match[2] : null; + if (implicitProtocol && !url.match(/^[a-zA-Z]+:\/\//)) { + url = `${implicitProtocol}://${url}`; + } + + // 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 + return { + protocol: match[2] || undefined, + domain: match[4] || undefined, + path: match[5] || undefined, + query: match[7] || undefined, + fragment: match[9] || undefined, + }; } } diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts index 8f0c7e1b6..74596aa20 100644 --- a/src/core/compile/providers/compile.ts +++ b/src/core/compile/providers/compile.ts @@ -55,6 +55,7 @@ import { Md5 } from 'ts-md5/dist/md5'; // Import core classes that can be useful for site plugins. import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreUrl } from '@classes/utils/url'; import { CoreCache } from '@classes/cache'; import { CoreDelegate } from '@classes/delegate'; import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; @@ -263,6 +264,7 @@ export class CoreCompileProvider { instance['moment'] = moment; instance['Md5'] = Md5; instance['CoreSyncBaseProvider'] = CoreSyncBaseProvider; + instance['CoreUrl'] = CoreUrl; instance['CoreCache'] = CoreCache; instance['CoreDelegate'] = CoreDelegate; instance['CoreContentLinksHandlerBase'] = CoreContentLinksHandlerBase; diff --git a/src/core/login/pages/site/site.ts b/src/core/login/pages/site/site.ts index b1b7dbd9b..27db57687 100644 --- a/src/core/login/pages/site/site.ts +++ b/src/core/login/pages/site/site.ts @@ -116,13 +116,13 @@ export class CoreLoginSitePage { // Attempt parsing the domain after initial check failed .catch((error) => { - const domain = CoreUrl.parseDomain(url); + const urlParts = CoreUrl.parse(url, 'http'); - if (!domain) { + if (!urlParts || !urlParts.domain) { throw error; } - return this.sitesProvider.checkSite(domain); + return this.sitesProvider.checkSite(urlParts.domain); }) .then((result) => this.login(result)) @@ -170,6 +170,13 @@ export class CoreLoginSitePage { modal.present(); } + /** + * Process login to a site. + * + * @param response Response obtained from the site check request. + * + * @return Promise resolved after logging in. + */ private async login(response: CoreSiteCheckResponse): Promise { return this.sitesProvider.checkRequiredMinimumVersion(response.config).then(() => { if (response.warning) { From a476a773f10264734ce1735d9690c3668f5a6de4 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 23 Jan 2020 10:33:43 +0100 Subject: [PATCH 3/3] 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);