diff --git a/src/classes/utils/url.ts b/src/classes/utils/url.ts new file mode 100644 index 000000000..2539d19c6 --- /dev/null +++ b/src/classes/utils/url.ts @@ -0,0 +1,122 @@ +// (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. + +/** + * Parts contained within a url. + */ +interface UrlParts { + + /** + * Url protocol. + */ + protocol?: string; + + /** + * Url domain. + */ + domain?: string; + + /** + * Url port. + */ + port?: 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 parts. + */ + 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; + } + + // 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: 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/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 f9bc3a139..5a231d27c 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. @@ -86,6 +87,8 @@ export class CoreLoginSitePage { return; } + url = url.trim(); + const modal = this.domUtils.showModalLoading(), siteData = this.sitesProvider.getDemoSiteData(url); @@ -111,27 +114,16 @@ 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) + .catch((error) => { + // Attempt guessing the domain if the initial check failed + const domain = CoreUrl.guessMoodleDomain(url); - 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 domain ? this.sitesProvider.checkSite(domain) : Promise.reject(error); + }) + .then((result) => this.login(result)) + .catch((error) => this.showLoginIssue(url, error)) + .finally(() => modal.dismiss()); } } @@ -173,4 +165,30 @@ 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. + */ + protected 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. + }); + } + }