diff --git a/assets/js/components/DetailsPermaLinks.js b/assets/js/components/DetailsPermaLinks.js index 5d04690a3ce..4dc4499250f 100644 --- a/assets/js/components/DetailsPermaLinks.js +++ b/assets/js/components/DetailsPermaLinks.js @@ -33,7 +33,7 @@ import { Fragment } from '@wordpress/element'; import Data from 'googlesitekit-data'; import { CORE_SITE } from '../googlesitekit/datastore/site/constants'; import Link from './Link'; -import getFullURL from '../util/getFullURL'; +import { getFullURL } from '../util'; const { useSelect } = Data; export default function DetailsPermaLinks( { title, path, serviceURL } ) { diff --git a/assets/js/components/data-table.js b/assets/js/components/data-table.js index 687eec4d627..dafab3fd811 100644 --- a/assets/js/components/data-table.js +++ b/assets/js/components/data-table.js @@ -29,8 +29,7 @@ import { */ import SourceLink from './SourceLink'; import Link from './Link'; -import getFullURL from '../util/getFullURL'; -import { getSiteKitAdminURL } from '../util'; +import { getFullURL, getSiteKitAdminURL } from '../util'; // Construct a table component from a data object. export const getDataTableFromData = ( data, headers, options ) => { @@ -157,4 +156,3 @@ export const getDataTableFromData = ( data, headers, options ) => { ); }; - diff --git a/assets/js/googlesitekit/datastore/site/info.js b/assets/js/googlesitekit/datastore/site/info.js index cc37dd0d5ab..efbb09e6b32 100644 --- a/assets/js/googlesitekit/datastore/site/info.js +++ b/assets/js/googlesitekit/datastore/site/info.js @@ -32,7 +32,7 @@ import { addQueryArgs, getQueryArg } from '@wordpress/url'; */ import Data from 'googlesitekit-data'; import { STORE_NAME, AMP_MODE_PRIMARY, AMP_MODE_SECONDARY } from './constants'; -import { getLocale } from '../../../util/i18n'; +import { getLocale, normalizeURL } from '../../../util'; const { createRegistrySelector } = Data; @@ -519,10 +519,6 @@ export const selectors = { */ isSiteURLMatch: createRegistrySelector( ( select ) => ( state, url ) => { const referenceURL = select( STORE_NAME ).getReferenceSiteURL(); - const normalizeURL = ( incomingURL ) => incomingURL - .replace( /^https?:\/\/(www\.)?/i, '' ) // Remove protocol and optional "www." prefix from the URL. - .replace( /\/$/, '' ); // Remove trailing slash. - return normalizeURL( referenceURL ) === normalizeURL( url ); } ), }; diff --git a/assets/js/modules/analytics-4/datastore/constants.js b/assets/js/modules/analytics-4/datastore/constants.js index fdcfa31b673..08ed8698ffe 100644 --- a/assets/js/modules/analytics-4/datastore/constants.js +++ b/assets/js/modules/analytics-4/datastore/constants.js @@ -20,3 +20,5 @@ export const STORE_NAME = 'modules/analytics-4'; export { STORE_NAME as MODULES_ANALYTICS_4 }; export const PROPERTY_CREATE = 'property_create'; + +export const MAX_WEBDATASTREAMS_PER_BATCH = 10; diff --git a/assets/js/modules/analytics-4/datastore/properties.js b/assets/js/modules/analytics-4/datastore/properties.js index 047b33706de..aae4e300b73 100644 --- a/assets/js/modules/analytics-4/datastore/properties.js +++ b/assets/js/modules/analytics-4/datastore/properties.js @@ -26,10 +26,12 @@ import invariant from 'invariant'; */ import API from 'googlesitekit-api'; import Data from 'googlesitekit-data'; -import { STORE_NAME, PROPERTY_CREATE } from './constants'; +import { STORE_NAME, PROPERTY_CREATE, MAX_WEBDATASTREAMS_PER_BATCH } from './constants'; +import { normalizeURL } from '../../../util'; import { createFetchStore } from '../../../googlesitekit/data/create-fetch-store'; import { isValidPropertySelection } from '../utils/validation'; import { actions as webDataStreamActions } from './webdatastreams'; +const { commonActions } = Data; const fetchGetPropertyStore = createFetchStore( { baseName: 'getProperty', @@ -162,6 +164,76 @@ const baseActions = { } }() ); }, + + /** + * Matches a property by URL. + * + * @since n.e.x.t + * + * @param {Array.} properties Array of property IDs. + * @param {Array.|string} url A list of URLs or a signle URL to match properties. + * @return {Object} A property object if found. + */ + *matchPropertyByURL( properties, url ) { + const registry = yield commonActions.getRegistry(); + const urls = ( Array.isArray( url ) ? url : [ url ] ).map( normalizeURL ); + + for ( let i = 0; i < properties.length; i += MAX_WEBDATASTREAMS_PER_BATCH ) { + const chunk = properties.slice( i, i + MAX_WEBDATASTREAMS_PER_BATCH ); + const webdatastreams = yield commonActions.await( + registry.__experimentalResolveSelect( STORE_NAME ).getWebDataStreamsBatch( chunk ), + ); + + for ( const propertyID in webdatastreams ) { + for ( const webdatastream of webdatastreams[ propertyID ] ) { + for ( const singleURL of urls ) { + if ( singleURL === normalizeURL( webdatastream.defaultUri ) ) { + return yield commonActions.await( + registry.__experimentalResolveSelect( STORE_NAME ).getProperty( propertyID ), + ); + } + } + } + } + } + + return null; + }, + + /** + * Matches a property by measurement ID. + * + * @since n.e.x.t + * + * @param {Array.} properties Array of property IDs. + * @param {Array.|string} measurementID A list of measurement IDs or a signle measurement ID to match properties. + * @return {Object} A property object if found. + */ + *matchPropertyByMeasurementID( properties, measurementID ) { + const registry = yield commonActions.getRegistry(); + const measurementIDs = Array.isArray( measurementID ) ? measurementID : [ measurementID ]; + + for ( let i = 0; i < properties.length; i += MAX_WEBDATASTREAMS_PER_BATCH ) { + const chunk = properties.slice( i, i + MAX_WEBDATASTREAMS_PER_BATCH ); + const webdatastreams = yield commonActions.await( + registry.__experimentalResolveSelect( STORE_NAME ).getWebDataStreamsBatch( chunk ), + ); + + for ( const propertyID in webdatastreams ) { + for ( const webdatastream of webdatastreams[ propertyID ] ) { + for ( const singleMeasurementID of measurementIDs ) { + if ( singleMeasurementID === webdatastream.measurementId ) { // eslint-disable-line sitekit/acronym-case + return yield commonActions.await( + registry.__experimentalResolveSelect( STORE_NAME ).getProperty( propertyID ), + ); + } + } + } + } + } + + return null; + }, }; const baseControls = { diff --git a/assets/js/modules/analytics-4/datastore/properties.test.js b/assets/js/modules/analytics-4/datastore/properties.test.js index bdea059f2a9..914ec2e2b8f 100644 --- a/assets/js/modules/analytics-4/datastore/properties.test.js +++ b/assets/js/modules/analytics-4/datastore/properties.test.js @@ -191,6 +191,52 @@ describe( 'modules/analytics-4 properties', () => { expect( registry.select( STORE_NAME ).getMeasurementID() ).toBe( fixtures.webDataStreams[ 1 ].measurementId ); // eslint-disable-line sitekit/acronym-case } ); } ); + + describe( 'matchPropertyByURL', () => { + const property = fixtures.properties[ 0 ]; + const propertyID = property._id; + const propertyIDs = [ propertyID ]; + + beforeEach( () => { + registry.dispatch( STORE_NAME ).receiveGetProperty( property, { propertyID } ); + registry.dispatch( STORE_NAME ).receiveGetWebDataStreamsBatch( fixtures.webDataStreamsBatch, { propertyIDs } ); + } ); + + it( 'should return a property object when a property is found', async () => { + const url = 'https://www.example.org/'; + const matchedProperty = await registry.dispatch( STORE_NAME ).matchPropertyByURL( propertyIDs, url ); + expect( matchedProperty ).toEqual( property ); + } ); + + it( 'should return NULL when a property is not found', async () => { + const url = 'https://www.example.io/'; + const matchedProperty = await registry.dispatch( STORE_NAME ).matchPropertyByURL( propertyIDs, url ); + expect( matchedProperty ).toBeNull(); + } ); + } ); + + describe( 'matchPropertyByMeasurementID', () => { + const property = fixtures.properties[ 0 ]; + const propertyID = property._id; + const propertyIDs = [ propertyID ]; + + beforeEach( () => { + registry.dispatch( STORE_NAME ).receiveGetProperty( property, { propertyID } ); + registry.dispatch( STORE_NAME ).receiveGetWebDataStreamsBatch( fixtures.webDataStreamsBatch, { propertyIDs } ); + } ); + + it( 'should return a property object when a property is found', async () => { + const measurementID = '1A2BCD346E'; + const matchedProperty = await registry.dispatch( STORE_NAME ).matchPropertyByMeasurementID( propertyIDs, measurementID ); + expect( matchedProperty ).toEqual( property ); + } ); + + it( 'should return NULL when a property is not found', async () => { + const measurementID = '0000000000'; + const matchedProperty = await registry.dispatch( STORE_NAME ).matchPropertyByMeasurementID( propertyIDs, measurementID ); + expect( matchedProperty ).toBeNull(); + } ); + } ); } ); describe( 'selectors', () => { diff --git a/assets/js/modules/analytics-4/datastore/webdatastreams.js b/assets/js/modules/analytics-4/datastore/webdatastreams.js index a92ea1a5f91..a0099e37b27 100644 --- a/assets/js/modules/analytics-4/datastore/webdatastreams.js +++ b/assets/js/modules/analytics-4/datastore/webdatastreams.js @@ -29,14 +29,12 @@ import difference from 'lodash/difference'; import API from 'googlesitekit-api'; import Data from 'googlesitekit-data'; import { createValidatedAction } from '../../../googlesitekit/data/utils'; -import { STORE_NAME } from './constants'; +import { STORE_NAME, MAX_WEBDATASTREAMS_PER_BATCH } from './constants'; import { CORE_SITE } from '../../../googlesitekit/datastore/site/constants'; import { createFetchStore } from '../../../googlesitekit/data/create-fetch-store'; import { isValidPropertyID } from '../utils/validation'; const { createRegistryControl, createRegistrySelector } = Data; -const MAX_WEBDATASTREAMS_PER_BATCH = 10; - const fetchGetWebDataStreamsStore = createFetchStore( { baseName: 'getWebDataStreams', controlCallback( { propertyID } ) { diff --git a/assets/js/modules/analytics/components/dashboard/DashboardAllTrafficWidget/index.js b/assets/js/modules/analytics/components/dashboard/DashboardAllTrafficWidget/index.js index 768ddce6de3..c6a72fc23ca 100644 --- a/assets/js/modules/analytics/components/dashboard/DashboardAllTrafficWidget/index.js +++ b/assets/js/modules/analytics/components/dashboard/DashboardAllTrafficWidget/index.js @@ -37,7 +37,7 @@ import { CORE_SITE } from '../../../../../googlesitekit/datastore/site/constants import { CORE_USER } from '../../../../../googlesitekit/datastore/user/constants'; import { CORE_UI } from '../../../../../googlesitekit/datastore/ui/constants'; import { Grid, Row, Cell } from '../../../../../material-components/layout'; -import { getURLPath } from '../../../../../util/getURLPath'; +import { getURLPath } from '../../../../../util'; import whenActive from '../../../../../util/when-active'; import SourceLink from '../../../../../components/SourceLink'; import TotalUserCount from './TotalUserCount'; @@ -45,7 +45,7 @@ import UserCountGraph from './UserCountGraph'; import DimensionTabs from './DimensionTabs'; import UserDimensionsPieChart from './UserDimensionsPieChart'; import { isZeroReport } from '../../../util'; -import { generateDateRangeArgs } from '../../../../analytics/util/report-date-range-args'; +import { generateDateRangeArgs } from '../../../util/report-date-range-args'; const { useSelect, useDispatch } = Data; diff --git a/assets/js/modules/analytics/components/dashboard/DashboardBounceRateWidget.js b/assets/js/modules/analytics/components/dashboard/DashboardBounceRateWidget.js index 6197cf870c2..51147f3c689 100644 --- a/assets/js/modules/analytics/components/dashboard/DashboardBounceRateWidget.js +++ b/assets/js/modules/analytics/components/dashboard/DashboardBounceRateWidget.js @@ -32,8 +32,7 @@ import whenActive from '../../../../util/when-active'; import PreviewBlock from '../../../../components/PreviewBlock'; import DataBlock from '../../../../components/DataBlock'; import Sparkline from '../../../../components/Sparkline'; -import { calculateChange } from '../../../../util'; -import { getURLPath } from '../../../../util/getURLPath'; +import { calculateChange, getURLPath } from '../../../../util'; import parseDimensionStringToDate from '../../util/parseDimensionStringToDate'; import { isZeroReport } from '../../util'; import { generateDateRangeArgs } from '../../util/report-date-range-args'; diff --git a/assets/js/modules/analytics/components/dashboard/DashboardSearchVisitorsWidget.js b/assets/js/modules/analytics/components/dashboard/DashboardSearchVisitorsWidget.js index 3fba2f3f7fe..cd750a1cd3e 100644 --- a/assets/js/modules/analytics/components/dashboard/DashboardSearchVisitorsWidget.js +++ b/assets/js/modules/analytics/components/dashboard/DashboardSearchVisitorsWidget.js @@ -32,8 +32,7 @@ import whenActive from '../../../../util/when-active'; import PreviewBlock from '../../../../components/PreviewBlock'; import DataBlock from '../../../../components/DataBlock'; import Sparkline from '../../../../components/Sparkline'; -import { calculateChange } from '../../../../util'; -import { getURLPath } from '../../../../util/getURLPath'; +import { calculateChange, getURLPath } from '../../../../util'; import parseDimensionStringToDate from '../../util/parseDimensionStringToDate'; import { isZeroReport } from '../../util'; import { generateDateRangeArgs } from '../../util/report-date-range-args'; diff --git a/assets/js/modules/analytics/components/dashboard/LegacyAnalyticsDashboardWidgetTopLevel.js b/assets/js/modules/analytics/components/dashboard/LegacyAnalyticsDashboardWidgetTopLevel.js index b6adf3fc537..d905a101cb5 100644 --- a/assets/js/modules/analytics/components/dashboard/LegacyAnalyticsDashboardWidgetTopLevel.js +++ b/assets/js/modules/analytics/components/dashboard/LegacyAnalyticsDashboardWidgetTopLevel.js @@ -33,6 +33,7 @@ import { Fragment, useState, useEffect } from '@wordpress/element'; import { getTimeInSeconds, calculateChange, + getURLPath, } from '../../../../util'; import extractForSparkline from '../../../../util/extract-for-sparkline'; import { @@ -45,7 +46,6 @@ import { userReportDataDefaults, parseTotalUsersData, } from '../../util'; -import { getURLPath } from '../../../../util/getURLPath'; import Data from 'googlesitekit-data'; import DataBlock from '../../../../components/DataBlock'; import withData from '../../../../components/higherorder/withData'; diff --git a/assets/js/modules/analytics/components/dashboard/LegacyDashboardAcquisitionPieChart.js b/assets/js/modules/analytics/components/dashboard/LegacyDashboardAcquisitionPieChart.js index 8569c579a96..ed4320a803e 100644 --- a/assets/js/modules/analytics/components/dashboard/LegacyDashboardAcquisitionPieChart.js +++ b/assets/js/modules/analytics/components/dashboard/LegacyDashboardAcquisitionPieChart.js @@ -33,7 +33,7 @@ import { __, _x, sprintf } from '@wordpress/i18n'; import Data from 'googlesitekit-data'; import { STORE_NAME } from '../../datastore/constants'; import { CORE_SITE } from '../../../../googlesitekit/datastore/site/constants'; -import { getTimeInSeconds } from '../../../../util'; +import { getTimeInSeconds, getURLPath } from '../../../../util'; import GoogleChart from '../../../../components/GoogleChart'; import withData from '../../../../components/higherorder/withData'; import { TYPE_MODULES } from '../../../../components/data'; @@ -45,7 +45,6 @@ import { trafficSourcesReportDataDefaults, isDataZeroForReporting, } from '../../util'; -import { getURLPath } from '../../../../util/getURLPath'; const { useSelect } = Data; diff --git a/assets/js/util/getFullURL.js b/assets/js/util/getFullURL.js deleted file mode 100644 index c42eeb0770c..00000000000 --- a/assets/js/util/getFullURL.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Absolute URL path getter utility function. - * - * Site Kit by Google, Copyright 2021 Google LLC - * - * 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 - * - * https://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. - */ - -/** - * Returns the absolute URL from a path including the siteURL. - * - * @since 1.32.0 - * - * @param {string} siteURL The siteURL fo the WordPress install. - * @param {string} path The path. - * @return {string} The URL path. - */ -export function getFullURL( siteURL, path ) { - return new URL( path, siteURL ).href; -} - -export default getFullURL; diff --git a/assets/js/util/getFullURL.test.js b/assets/js/util/getFullURL.test.js deleted file mode 100644 index 87a20b8eb84..00000000000 --- a/assets/js/util/getFullURL.test.js +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Absolute URL path getter utility function tests. - * - * Site Kit by Google, Copyright 2021 Google LLC - * - * 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 - * - * https://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. - */ - -/** - * Internal dependencies - */ -import { getFullURL } from './getFullURL'; - -describe( 'getFullURL', () => { - it( 'returns the URL with path', () => { - expect( - getFullURL( 'https://www.example.com', '' ) - ).toEqual( 'https://www.example.com/' ); - - expect( - getFullURL( 'https://www.example.com', '/path' ) - ).toEqual( 'https://www.example.com/path' ); - - expect( - getFullURL( 'https://www.example.com/slug/slug', '/path' ) - ).toEqual( 'https://www.example.com/path' ); - - expect( - getFullURL( 'https://www.example.com:444/slug/slug', '/path' ) - ).toEqual( 'https://www.example.com:444/path' ); - - expect( - getFullURL( 'https://www.firstexample.com/slug', 'https://www.secondexample.com/path' ) - ).toEqual( 'https://www.secondexample.com/path' ); - - expect( - getFullURL( 'https://www.firstexample.com/slug', 'https://www.secondexample.com:9000/path' ) - ).toEqual( 'https://www.secondexample.com:9000/path' ); - } ); - - it( 'throws if not a valid URL and/or path', () => { - expect( - () => { - getFullURL( false, false ); - } - ).toThrow( 'Invalid base URL: false' ); - - expect( - () => { - getFullURL( '/slug', '/path' ); - } - ).toThrow( 'Invalid base URL:' ); - - expect( - () => { - getFullURL( '', 'https://www.example.com' ); - } - ).toThrow( 'Invalid base URL:' ); - - expect( - () => { - getFullURL( '/slug', 'https://www.example.com' ) - ; - } - ).toThrow( 'Invalid base URL: /slug' ); - } ); -} ); diff --git a/assets/js/util/getURLPath.test.js b/assets/js/util/getURLPath.test.js deleted file mode 100644 index b901064dd2d..00000000000 --- a/assets/js/util/getURLPath.test.js +++ /dev/null @@ -1,72 +0,0 @@ -/** - * URL pathname getter utility function tests. - * - * Site Kit by Google, Copyright 2021 Google LLC - * - * 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 - * - * https://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. - */ - -/** - * Internal dependencies - */ -import { getURLPath } from './getURLPath'; - -describe( 'getURLPath', () => { - it( 'returns only the path of a URL', () => { - expect( - getURLPath( 'http://example.com/foobar' ) - ).toEqual( '/foobar' ); - - expect( - getURLPath( 'http://example.com/' ) - ).toEqual( '/' ); - - expect( - getURLPath( 'http://example.com' ) - ).toEqual( '/' ); - - expect( - getURLPath( 'http://example.com:3333/foo/bar.html?query=string&test#heading' ) - ).toEqual( '/foo/bar.html' ); - } ); - - it( 'throws if not a valid URL', () => { - expect( - () => { - getURLPath( false ) - ; - } - ).toThrow( 'Invalid URL' ); - - expect( - () => { - getURLPath( null ) - ; - } - ).toThrow( 'Invalid URL' ); - - expect( - () => { - getURLPath( '' ) - ; - } - ).toThrow( 'Invalid URL' ); - - expect( - () => { - getURLPath( 'foo.com/test' ) - ; - } - ).toThrow( 'Invalid URL' ); - } ); -} ); diff --git a/assets/js/util/index.js b/assets/js/util/index.js index dd78f8d6c87..d7bc1c7e9ce 100644 --- a/assets/js/util/index.js +++ b/assets/js/util/index.js @@ -19,11 +19,8 @@ /** * External dependencies */ -import { - isFinite, - get, - unescape, -} from 'lodash'; +import isFinite from 'lodash/isFinite'; +import unescape from 'lodash/unescape'; /** * WordPress dependencies @@ -47,6 +44,7 @@ export * from './markdown'; export * from './convert-time'; export * from './date-range'; export * from './chart'; +export * from './urls'; /** * Removes a parameter from a URL string. @@ -99,27 +97,6 @@ export const removeURLParameter = ( url, parameter ) => { return parsedURL.href; }; -/** - * Gets the current locale for use with browser APIs. - * - * @since 1.6.0 - * - * @param {Object} _global The global window object. - * @return {string} Current Site Kit locale if set, otherwise the current language set by the browser. - * E.g. `en-US` or `de-DE` - */ -export const getLocale = ( _global = global ) => { - const siteKitLocale = get( _global, [ '_googlesitekitLegacyData', 'locale', '', 'lang' ] ); - if ( siteKitLocale ) { - const matches = siteKitLocale.match( /^(\w{2})?(_)?(\w{2})/ ); - if ( matches && matches[ 0 ] ) { - return matches[ 0 ].replace( /_/g, '-' ); - } - } - - return _global.navigator.language; -}; - /** * Transforms a period string into a number of seconds. * diff --git a/assets/js/util/test/getLocale.js b/assets/js/util/test/getLocale.js deleted file mode 100644 index 733f8ecd6b8..00000000000 --- a/assets/js/util/test/getLocale.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Internal dependencies - */ -import { getLocale } from '../'; - -describe( 'getLocale', () => { - const siteKitLocales = [ - [ - 'en', - 'en', - ], - [ - 'en_CA', - 'en-CA', - ], - [ - 'de_DE_formal', - 'de-DE', - ], - [ - 'de_CH_informal', - 'de-CH', - ], - [ - 'pt_PT_ao90', - 'pt-PT', - ], - [ - 'sr-Latn-RS', - 'sr', - ], - ]; - - it.each( siteKitLocales )( 'Site Kit locale %s returns as %s', ( value, expected ) => { - expect( getLocale( - { - _googlesitekitLegacyData: { - locale: { - '': { - lang: value, - }, - }, - }, - } - ) ).toStrictEqual( expected ); - } ); -} ); diff --git a/assets/js/util/getURLPath.js b/assets/js/util/urls.js similarity index 56% rename from assets/js/util/getURLPath.js rename to assets/js/util/urls.js index bf741a3e9f0..4fdd69265cc 100644 --- a/assets/js/util/getURLPath.js +++ b/assets/js/util/urls.js @@ -28,4 +28,29 @@ export function getURLPath( url ) { return new URL( url ).pathname; } -export default getURLPath; +/** + * Returns the absolute URL from a path including the siteURL. + * + * @since 1.32.0 + * + * @param {string} siteURL The siteURL fo the WordPress install. + * @param {string} path The path. + * @return {string} The URL path. + */ +export function getFullURL( siteURL, path ) { + return new URL( path, siteURL ).href; +} + +/** + * Normalizes URL by removing protocol, www subdomain and trailing slash. + * + * @since n.e.x.t + * + * @param {string} incomingURL The original URL. + * @return {string} Normalized URL. + */ +export function normalizeURL( incomingURL ) { + return incomingURL + .replace( /^https?:\/\/(www\.)?/i, '' ) // Remove protocol and optional "www." prefix from the URL. + .replace( /\/$/, '' ); // Remove trailing slash. +} diff --git a/assets/js/util/urls.test.js b/assets/js/util/urls.test.js new file mode 100644 index 00000000000..4c26092867c --- /dev/null +++ b/assets/js/util/urls.test.js @@ -0,0 +1,74 @@ +/** + * URL pathname getter utility function tests. + * + * Site Kit by Google, Copyright 2021 Google LLC + * + * 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 + * + * https://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. + */ + +/** + * Internal dependencies + */ +import { getURLPath, getFullURL, normalizeURL } from './urls'; + +describe( 'getURLPath', () => { + it.each( [ + [ '/foobar', 'http://example.com/foobar' ], + [ '/', 'http://example.com/' ], + [ '/', 'http://example.com' ], + [ '/foo/bar.html', 'http://example.com:3333/foo/bar.html?query=string&test#heading' ], + ] )( 'should return %s for %s', ( expected, url ) => { + expect( getURLPath( url ) ).toBe( expected ); + } ); + + it.each( [ + [ 'FALSE', false ], + [ 'NULL', null ], + [ 'an empty string', '' ], + [ 'incomplete URL', 'foo.com/test' ], + ] )( 'should throw an error if "%s" is passed instead of a valid URL', ( _, val ) => { + expect( () => getURLPath( val ) ).toThrow( 'Invalid URL' ); + } ); +} ); + +describe( 'getFullURL', () => { + it.each( [ + [ 'https://www.example.com', '', 'https://www.example.com/' ], + [ 'https://www.example.com', '/path', 'https://www.example.com/path' ], + [ 'https://www.example.com/slug/slug', '/path', 'https://www.example.com/path' ], + [ 'https://www.example.com:444/slug/slug', '/path', 'https://www.example.com:444/path' ], + [ 'https://www.firstexample.com/slug', 'https://www.secondexample.com/path', 'https://www.secondexample.com/path' ], + [ 'https://www.firstexample.com/slug', 'https://www.secondexample.com:9000/path', 'https://www.secondexample.com:9000/path' ], + ] )( 'should return the correct URL when "%s" and "%s" are passed', ( siteURL, path, expected ) => { + expect( getFullURL( siteURL, path ) ).toBe( expected ); + } ); + + it.each( [ + [ 'falsy site URL and falsy path are passed', false, false ], + [ 'incomplete URL is passed', '/slug', '/path' ], + [ 'site URL is passed as path parameter', '', 'https://www.example.com' ], + ] )( 'should throw an error if %s', ( _, siteURL, path ) => { + expect( () => getFullURL( siteURL, path ) ).toThrow(); + } ); +} ); + +describe( 'normalizeURL', () => { + it.each( [ + [ 'https://example.com', 'example.com' ], + [ 'http://example.com/', 'example.com' ], + [ 'http://www.example.com/', 'example.com' ], + [ 'http://www.example.com/slug/', 'example.com/slug' ], + ] )( 'should normalize %s to %s', ( url, expected ) => { + expect( normalizeURL( url ) ).toBe( expected ); + } ); +} );