Skip to content

Commit

Permalink
fix(UVE): Page tools not working with traditional rendering (#29602)
Browse files Browse the repository at this point in the history
### Proposed Changes
* **Fix Placeholder Replacement**: Updated the `getRunnableLink`
function to ensure placeholders `{requestHostName}`, `{currentUrl}`, and
`{urlSearchParams}` are replaced correctly in the URL.
* **Handle Null and Empty Values**: Improved handling of `null` and
empty values for `currentUrl` and `requestHostName` to avoid unintended
`null` values in the final URL.
* **Query Parameters Formatting**: Corrected the formatting and
appending of query parameters (`host_id` and `language_id`) to ensure
they are correctly included in the URL when provided.
* **Update Deprecated URL**: Updated the URL for Mozilla Observatory,
which has been deprecated.

### Additional Info

This PR addresses those issues by fixing how search parameters are
formatted and applied, ensuring that page tools can properly recognize
and utilize the `host_id` parameter.

### Differences

**Wave URL Before:**

https://wave.webaim.org/report#/http://localhost:4200/page-for-my-site?host_id=50a79decd9e21702cb2f52fc4935a52b?language_id=1

**Wave URL After:**

https://wave.webaim.org/report#/http://localhost:4200/page-for-my-site?host_id=50a79decd9e21702cb2f52fc4935a52b&language_id=1

**Difference:** 
<img width="837" alt="image"
src="https://github.com/user-attachments/assets/2809fb96-646c-4de7-a6c2-587322072480">

The query parameters in the "Before" URL used a `?` to separate
`host_id` and `language_id`, whereas the "After" URL correctly uses `&`.

---

**Security Headers Before:**

https://securityheaders.com/?q=http://localhost:4200/page-for-my-site&host_id=50a79decd9e21702cb2f52fc4935a52b&language_id=1&followRedirects=on

**Security Headers After:**

https://securityheaders.com/?q=http://localhost:4200/page-for-my-site?host_id=50a79decd9e21702cb2f52fc4935a52b&language_id=1&followRedirects=on

**Difference:** 
<img width="955" alt="image"
src="https://github.com/user-attachments/assets/9bc06c96-6e91-4ee1-8be1-b23759ddbd1e">

In the "Before" URL, the `?` was incorrectly placed before the query
parameters, while the "After" URL correctly places `?` only after the
base URL, and `&` separates the query parameters.

This PR fixes #29206
  • Loading branch information
valentinogiardino authored Aug 15, 2024
1 parent 6ae4f6a commit 70974ab
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 28 deletions.
6 changes: 3 additions & 3 deletions core-web/apps/dotcms-ui/src/assets/seo/page-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@
"title": "Wave",
"description": "The WAVE® evaluation suite helps educate authors on how to make their web content more accessible to individuals with disabilities. WAVE can identify many accessibility and Web Content Accessibility Guideline (WCAG) errors, but also facilitates human evaluation of web content.",
"tags": ["Accessibility", "WCAG"],
"runnableLink": "https://wave.webaim.org/report#/{requestHostName}{currentUrl}{siteId}{languageId}"
"runnableLink": "https://wave.webaim.org/report#/{requestHostName}{currentUrl}{urlSearchParams}"
},
{
"icon": "assets/seo/mozilla.png",
"title": "Mozilla Observatory",
"description": "The Mozilla Observatory has helped hundreds of thousands of websites by teaching developers, system administrators, and security professionals how to configure their sites safely and securely. ",
"tags": ["Security", "Best Practices"],
"runnableLink": "https://observatory.mozilla.org/analyze/{requestHostName}"
"runnableLink": "https://developer.mozilla.org/en-US/observatory/analyze?host={requestHostName}"
},
{
"icon": "assets/seo/security-headers.png",
"title": "Security Headers",
"description": "This tool is designed to help you better deploy and understand modern security features that are available for your website. It will provide a simple to understand grading system for how well your site follows best practices, as well as suggestions for how to make improvement.",
"tags": ["Security", "Best Practices"],
"runnableLink": "https://securityheaders.com/?q={requestHostName}{currentUrl}{siteId}{languageId}&followRedirects=on"
"runnableLink": "https://securityheaders.com/?q={requestHostName}{currentUrl}{urlSearchParams}&followRedirects=on"
}
]
}
179 changes: 177 additions & 2 deletions core-web/libs/utils/src/lib/dot-utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DotCMSBaseTypesContentTypes } from '@dotcms/dotcms-models';
import { DotCMSBaseTypesContentTypes, DotPageToolUrlParams } from '@dotcms/dotcms-models';
import { EMPTY_CONTENTLET } from '@dotcms/utils-testing';

import { getImageAssetUrl, ellipsizeText } from './dot-utils';
import { getImageAssetUrl, ellipsizeText, getRunnableLink } from './dot-utils';

describe('Dot Utils', () => {
describe('getImageAssetUrl', () => {
Expand Down Expand Up @@ -122,3 +122,178 @@ describe('Ellipsize Text Utility', () => {
expect(ellipsizeText(text, 15)).toEqual(truncated);
});
});

describe('Dot Utils', () => {
describe('getRunnableLink', () => {
it('should replace {requestHostName} with the actual requestHostName in WAVE URL', () => {
const url =
'https://wave.webaim.org/report#/{requestHostName}{currentUrl}{urlSearchParams}';
const params: DotPageToolUrlParams = {
currentUrl: '',
requestHostName: 'my-site',
siteId: '',
languageId: 1
};

expect(getRunnableLink(url, params)).toEqual(
'https://wave.webaim.org/report#/my-site?language_id=1'
);
});

it('should replace {currentUrl} and {requestHostName} in WAVE URL and append query parameters', () => {
const url =
'https://wave.webaim.org/report#/{requestHostName}{currentUrl}{urlSearchParams}';
const params: DotPageToolUrlParams = {
currentUrl: '/current-page',
requestHostName: 'my-site',
siteId: '50a79decd9e21702cb2f52fc4935a52b',
languageId: 1
};

expect(getRunnableLink(url, params)).toEqual(
'https://wave.webaim.org/report#/my-site/current-page?host_id=50a79decd9e21702cb2f52fc4935a52b&language_id=1'
);
});

it('should replace {requestHostName} and append query parameters in Mozilla Observatory URL', () => {
const url =
'https://developer.mozilla.org/en-US/observatory/analyze?host={requestHostName}';
const params: DotPageToolUrlParams = {
currentUrl: '',
requestHostName: 'my-site',
siteId: '50a79decd9e21702cb2f52fc4935a52b',
languageId: 1
};

expect(getRunnableLink(url, params)).toEqual(
'https://developer.mozilla.org/en-US/observatory/analyze?host=my-site'
);
});

it('should replace {requestHostName}, {currentUrl} and append query parameters in Security Headers URL', () => {
const url =
'https://securityheaders.com/?q={requestHostName}{currentUrl}{urlSearchParams}&followRedirects=on';
const params: DotPageToolUrlParams = {
currentUrl: '/current-page',
requestHostName: 'my-site',
siteId: '50a79decd9e21702cb2f52fc4935a52b',
languageId: 1
};

expect(getRunnableLink(url, params)).toEqual(
'https://securityheaders.com/?q=my-site/current-page?host_id=50a79decd9e21702cb2f52fc4935a52b&language_id=1&followRedirects=on'
);
});

it('should handle empty URL correctly', () => {
const url = '';
const params: DotPageToolUrlParams = {
currentUrl: '',
requestHostName: '',
siteId: '',
languageId: 1
};

expect(getRunnableLink(url, params)).toEqual('');
});
it('should replace {requestHostName} with the actual requestHostName', () => {
const url = 'https://example.com/{requestHostName}/page';
const params: DotPageToolUrlParams = {
currentUrl: '',
requestHostName: 'my-site',
siteId: '',
languageId: 1
};

expect(getRunnableLink(url, params)).toEqual('https://example.com/my-site/page');
});

it('should replace {currentUrl} with the actual currentUrl', () => {
const url = 'https://example.com{currentUrl}/page';
const params: DotPageToolUrlParams = {
currentUrl: '/current-page',
requestHostName: '',
siteId: '',
languageId: 1
};

expect(getRunnableLink(url, params)).toEqual('https://example.com/current-page/page');
});

it('should append query parameters when siteId and languageId are provided', () => {
const url = 'https://example.com/page{urlSearchParams}';
const params: DotPageToolUrlParams = {
currentUrl: '',
requestHostName: '',
siteId: '123',
languageId: 456
};

expect(getRunnableLink(url, params)).toEqual(
'https://example.com/page?host_id=123&language_id=456'
);
});

it('should replace all placeholders and append query parameters correctly', () => {
const url = 'https://example.com/{requestHostName}{currentUrl}{urlSearchParams}';
const params: DotPageToolUrlParams = {
currentUrl: '/current-page',
requestHostName: 'my-site',
siteId: '123',
languageId: 456
};

expect(getRunnableLink(url, params)).toEqual(
'https://example.com/my-site/current-page?host_id=123&language_id=456'
);
});

it('should handle empty URL correctly', () => {
const url = '';
const params: DotPageToolUrlParams = {
currentUrl: '',
requestHostName: '',
siteId: '',
languageId: 1
};

expect(getRunnableLink(url, params)).toEqual('');
});

it('should handle null currentUrl and requestHostName', () => {
const url = 'https://example.com/{requestHostName}{currentUrl}{urlSearchParams}';
const params: DotPageToolUrlParams = {
currentUrl: null, // Handle null by substituting with empty string
requestHostName: null, // Handle null by substituting with empty string
siteId: '',
languageId: 1
};

expect(getRunnableLink(url, params)).toEqual('https://example.com/?language_id=1');
});

it('should handle null siteId and languageId', () => {
const url = 'https://example.com/page{urlSearchParams}';
const params: DotPageToolUrlParams = {
currentUrl: '',
requestHostName: '',
siteId: null, // Handle null by not appending the query parameter
languageId: null // Handle null by not appending the query parameter
};

expect(getRunnableLink(url, params)).toEqual('https://example.com/page');
});

it('should handle all parameters as null or empty', () => {
const url = 'https://example.com/{requestHostName}{currentUrl}{urlSearchParams}';
const params: DotPageToolUrlParams = {
currentUrl: null, // Handle null by substituting with empty string
requestHostName: null, // Handle null by substituting with empty string
siteId: null, // Handle null by not appending the query parameter
languageId: null // Handle null by not appending the query parameter
};

expect(getRunnableLink(url, params)).toEqual('https://example.com/');
});
});
});
54 changes: 31 additions & 23 deletions core-web/libs/utils/src/lib/dot-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,34 +41,42 @@ export function generateDotFavoritePageUrl(params: {
}

/**
* Get the query parameter separator
* @param url
* @returns
*/
export function getQueryParameterSeparator(url: string): string {
const regex = /[?&]/;

return url.match(regex) ? '&' : '?';
}

/**
* This method is used to get the runnable link for the tool
* @param url
* @returns
* Generates a runnable link by replacing placeholders in the base URL and appending query parameters.
*
* This function takes a base URL that may contain placeholders for the request host name and current URL,
* and it replaces these placeholders with actual values. Additionally, it appends optional query parameters
* based on the properties in the `currentPageUrlParams` object.
*
* @param url - The base URL for the tool, which may contain placeholders such as `{requestHostName}` and `{currentUrl}`.
* @param currentPageUrlParams - An object containing values to replace in the base URL and to add as query parameters:
* - `currentUrl` (optional): The URL to replace the `{currentUrl}` placeholder in the base URL.
* - `requestHostName` (optional): The host name to replace the `{requestHostName}` placeholder in the base URL.
* - `siteId` (optional): The site ID to include as a query parameter (`host_id`).
* - `languageId` (optional): The language ID to include as a query parameter (`language_id`).
*
* @returns A string representing the constructed URL with placeholders replaced and query parameters appended.
*/
export function getRunnableLink(url: string, currentPageUrlParams: DotPageToolUrlParams): string {
// If URL is empty, return an empty string
if (!url) return '';
// Destructure properties from currentPageUrlParams
const { currentUrl, requestHostName, siteId, languageId } = currentPageUrlParams;

url = url
.replace('{requestHostName}', requestHostName ?? '')
.replace('{currentUrl}', currentUrl ?? '')
.replace('{siteId}', siteId ? `${getQueryParameterSeparator(url)}host_id=${siteId}` : '')
.replace(
'{languageId}',
languageId ? `${getQueryParameterSeparator(url)}language_id=${String(languageId)}` : ''
);
// Create a URLSearchParams object to manage query parameters
const pageParams = new URLSearchParams();

// Append site ID and language ID as query parameters if they are provided
if (siteId) pageParams.append('host_id', siteId);
if (languageId) pageParams.append('language_id', String(languageId));

// Replace placeholders in the base URL with actual values and append query parameters if they exist
const finalUrl = url
.replace(/{requestHostName}/g, requestHostName ?? '')
.replace(/{currentUrl}/g, currentUrl ?? '')
.replace(/{urlSearchParams}/g, pageParams.toString() ? `?${pageParams.toString()}` : '');

return url;
// Create a URL object from the finalUrl and return its fully qualified and normalized form.
return new URL(finalUrl).toString();
}

/**
Expand Down

0 comments on commit 70974ab

Please sign in to comment.