diff --git a/src/app/app.component.test.ts b/src/app/app.component.test.ts index 1a8d32c16..e815b64bd 100644 --- a/src/app/app.component.test.ts +++ b/src/app/app.component.test.ts @@ -22,6 +22,7 @@ import { Network, Platform, NgZone } from '@singletons'; import { mockSingleton, renderComponent } from '@/testing/utils'; import { CoreNavigator, CoreNavigatorService } from '@services/navigator'; +import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; describe('AppComponent', () => { @@ -33,6 +34,7 @@ describe('AppComponent', () => { mockSingleton(Network, { onChange: () => new Observable() }); mockSingleton(Platform, { ready: () => Promise.resolve(), resume: new Subject() }); mockSingleton(NgZone, { run: jest.fn() }); + mockSingleton(CoreSitePlugins, { hasSitePluginsLoaded: false }); navigator = mockSingleton(CoreNavigator, ['navigate']); langProvider = mockSingleton(CoreLang, ['clearCustomStrings']); diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index 3803dbe44..958de26c1 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -78,6 +78,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 { CoreArray } from '@singletons/array'; +import { CoreText } from '@singletons/text'; import { CoreUrl } from '@singletons/url'; import { CoreWindow } from '@singletons/window'; import { CoreCache } from '@classes/cache'; @@ -340,6 +341,7 @@ export class CoreCompileProvider { instance['Md5'] = Md5; instance['CoreSyncBaseProvider'] = CoreSyncBaseProvider; instance['CoreArray'] = CoreArray; + instance['CoreText'] = CoreText; instance['CoreUrl'] = CoreUrl; instance['CoreWindow'] = CoreWindow; instance['CoreCache'] = CoreCache; diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index 299be92e0..80a940dcb 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -52,6 +52,7 @@ import { CoreArray } from '../singletons/array'; import { CoreNetworkError } from '@classes/errors/network-error'; import { CoreNavigationOptions } from './navigator'; import { CoreSitesFactory } from './sites-factory'; +import { CoreText } from '@singletons/text'; export const CORE_SITE_SCHEMAS = new InjectionToken('CORE_SITE_SCHEMAS'); @@ -1635,10 +1636,10 @@ export class CoreSitesProvider { // If more than one site is returned it usually means there are different users stored. Use any of them. const site = await this.getSite(siteIds[0]); - const siteUrl = CoreTextUtils.removeEndingSlash( + const siteUrl = CoreText.removeEndingSlash( CoreUrlUtils.removeProtocolAndWWW(site.getURL()), ); - const treatedUrl = CoreTextUtils.removeEndingSlash(CoreUrlUtils.removeProtocolAndWWW(url)); + const treatedUrl = CoreText.removeEndingSlash(CoreUrlUtils.removeProtocolAndWWW(url)); if (siteUrl == treatedUrl) { result.site = site; diff --git a/src/core/services/utils/text.ts b/src/core/services/utils/text.ts index 310e12b3b..cc8428e69 100644 --- a/src/core/services/utils/text.ts +++ b/src/core/services/utils/text.ts @@ -25,6 +25,7 @@ import { Locutus } from '@singletons/locutus'; import { CoreViewerTextComponent } from '@features/viewer/components/text/text'; import { CoreFileHelper } from '@services/file-helper'; import { CoreDomUtils } from './dom'; +import { CoreText } from '@singletons/text'; /** * Different type of errors the app can treat. @@ -710,21 +711,10 @@ export class CoreTextUtilsProvider { } /** - * Remove ending slash from a path or URL. - * - * @param text Text to treat. - * @return Treated text. + * @deprecated Use CoreText instead. */ removeEndingSlash(text?: string): string { - if (!text) { - return ''; - } - - if (text.slice(-1) == '/') { - return text.substr(0, text.length - 1); - } - - return text; + return CoreText.removeEndingSlash(text); } /** diff --git a/src/core/singletons/tests/url.test.ts b/src/core/singletons/tests/url.test.ts new file mode 100644 index 000000000..b15d0e918 --- /dev/null +++ b/src/core/singletons/tests/url.test.ts @@ -0,0 +1,59 @@ +// (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 { CoreUrl } from '@singletons/url'; + +describe('CoreUrl singleton', () => { + + it('parses standard urls', () => { + expect(CoreUrl.parse('https://my.subdomain.com/path/?query=search#hash')).toEqual({ + protocol: 'https', + domain: 'my.subdomain.com', + path: '/path/', + query: 'query=search', + fragment: 'hash', + }); + }); + + it('parses domains without TLD', () => { + expect(CoreUrl.parse('ftp://localhost/nested/path')).toEqual({ + protocol: 'ftp', + domain: 'localhost', + path: '/nested/path', + }); + }); + + it('parses ips', () => { + expect(CoreUrl.parse('http://192.168.1.157:8080/')).toEqual({ + protocol: 'http', + domain: '192.168.1.157', + port: '8080', + path: '/', + }); + }); + + it('compares domains and paths', () => { + expect(CoreUrl.sameDomainAndPath('https://school.edu', 'https://school.edu')).toBe(true); + expect(CoreUrl.sameDomainAndPath('https://school.edu', 'HTTPS://SCHOOL.EDU')).toBe(true); + expect(CoreUrl.sameDomainAndPath('https://school.edu/moodle', 'https://school.edu/moodle')).toBe(true); + expect(CoreUrl.sameDomainAndPath('https://school.edu/moodle', 'https://school.edu/moodle/')).toBe(true); + expect(CoreUrl.sameDomainAndPath('https://school.edu/moodle', 'https://school.edu/moodle#about')).toBe(true); + expect(CoreUrl.sameDomainAndPath('https://school.edu/moodle', 'HTTPS://SCHOOL.EDU/MOODLE')).toBe(true); + expect(CoreUrl.sameDomainAndPath('https://school.edu/moodle', 'HTTPS://SCHOOL.EDU/MOODLE/')).toBe(true); + expect(CoreUrl.sameDomainAndPath('https://school.edu/moodle', 'HTTPS://SCHOOL.EDU/MOODLE#ABOUT')).toBe(true); + + expect(CoreUrl.sameDomainAndPath('https://school.edu/moodle', 'https://school.edu/moodle/about')).toBe(false); + }); + +}); diff --git a/src/core/singletons/text.ts b/src/core/singletons/text.ts new file mode 100644 index 000000000..c95fa2a5b --- /dev/null +++ b/src/core/singletons/text.ts @@ -0,0 +1,43 @@ +// (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. + +/** + * Singleton with helper functions for text manipulation. + */ +export class CoreText { + + // Avoid creating singleton instances. + private constructor() { + // Nothing to do. + } + + /** + * Remove ending slash from a path or URL. + * + * @param text Text to treat. + * @return Treated text. + */ + static removeEndingSlash(text?: string): string { + if (!text) { + return ''; + } + + if (text.slice(-1) == '/') { + return text.substr(0, text.length - 1); + } + + return text; + } + +} diff --git a/src/core/singletons/url.ts b/src/core/singletons/url.ts index 2f38ffbe5..5c8b1f10f 100644 --- a/src/core/singletons/url.ts +++ b/src/core/singletons/url.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from './text'; /** * Parts contained within a url. @@ -196,8 +196,11 @@ export class CoreUrl { const partsA = CoreUrl.parse(urlA); const partsB = CoreUrl.parse(urlB); - return partsA?.domain == partsB?.domain && - CoreTextUtils.removeEndingSlash(partsA?.path) == CoreTextUtils.removeEndingSlash(partsB?.path); + partsA && Object.entries(partsA).forEach(([part, value]) => partsA[part] = value?.toLowerCase()); + partsB && Object.entries(partsB).forEach(([part, value]) => partsB[part] = value?.toLowerCase()); + + return partsA?.domain === partsB?.domain + && CoreText.removeEndingSlash(partsA?.path) === CoreText.removeEndingSlash(partsB?.path); } } diff --git a/src/testing/setup.ts b/src/testing/setup.ts index 9972e2e8e..6bffcfaca 100644 --- a/src/testing/setup.ts +++ b/src/testing/setup.ts @@ -21,6 +21,12 @@ console.debug = () => { // Silence. }; +// eslint-disable-next-line no-console, jest/no-jasmine-globals, @typescript-eslint/no-explicit-any +console.error = (...args: any[]) => fail(args.map(a => a.toString()).join('')); + +// eslint-disable-next-line jest/no-jasmine-globals +process.on('unhandledRejection', error => fail(error)); + // Override the method to create singleton method proxies in order to facilitate setting up // test expectations about method calls. setCreateSingletonMethodProxy(