Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions packages/angular/build/src/utils/server-rendering/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,53 @@ export function isSsrRequestHandler(
return typeof value === 'function' && '__ng_request_handler__' in value;
}

/**
* A mapping of the characters which have to be escaped to be interpolated into HTML,
* to their entity equivalents.
*/
const HTML_ESCAPE_CHARACTER_MAP: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
};

/**
* Escapes the characters of a value which is interpolated into HTML text or into a quoted
* attribute value.
*
* @param text - The value to escape.
* @returns The escaped value.
*/
function escapeHtml(text: string): string {
return text.replace(/[&<>"']/g, (character) => HTML_ESCAPE_CHARACTER_MAP[character]);
}
Comment thread
SkyZeroZx marked this conversation as resolved.

/**
* Generates a static HTML page with a meta refresh tag to redirect the user to a specified URL.
*
* This function creates a simple HTML page that performs a redirect using a meta tag.
* It includes a fallback link in case the meta-refresh doesn't work.
*
* The provided URL is HTML-escaped before being interpolated.
*
* @param url - The URL to which the page should redirect.
* @returns The HTML content of the static redirect page.
*/
export function generateRedirectStaticPage(url: string): string {
const escapedUrl = escapeHtml(url);

return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Redirecting</title>
<meta http-equiv="refresh" content="0; url=${url}">
<meta http-equiv="refresh" content="0; url=${escapedUrl}">
</head>
<body>
<pre>Redirecting to <a href="${url}">${url}</a></pre>
<pre>Redirecting to <a href="${escapedUrl}">${escapedUrl}</a></pre>
</body>
</html>
`.trim();
Expand Down
34 changes: 34 additions & 0 deletions packages/angular/build/src/utils/server-rendering/utils_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { generateRedirectStaticPage } from './utils';

describe('generateRedirectStaticPage', () => {
it('escapes the ampersands of a redirect target', () => {
const page = generateRedirectStaticPage('https://example.com/docs?from=ssg&next=/ssg');

expect(page).toContain(
'<meta http-equiv="refresh" content="0; url=https://example.com/docs?from=ssg&amp;next=/ssg">',
);
expect(page).toContain(
'<a href="https://example.com/docs?from=ssg&amp;next=/ssg">' +
'https://example.com/docs?from=ssg&amp;next=/ssg</a>',
);
});

it('escapes characters which would break out of the attribute or the tag', () => {
const page = generateRedirectStaticPage(`/"><script>alert('1')</script>`);

expect(page).not.toContain('<script>');
expect(page).toContain(
'<meta http-equiv="refresh" content="0; url=' +
'/&quot;&gt;&lt;script&gt;alert(&#39;1&#39;)&lt;/script&gt;">',
);
expect(page).toContain('<a href="/&quot;&gt;&lt;script&gt;alert(&#39;1&#39;)&lt;/script&gt;">');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export default async function () {
path: 'ssg-redirect',
redirectTo: 'ssg'
},
{
path: 'ssg-redirect-external',
component: Ssg,
},
{
path: 'ssg-redirect-via-guard',
canActivate: [() => {
Expand All @@ -73,6 +77,11 @@ export default async function () {
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
{
path: 'ssg-redirect-external',
renderMode: RenderMode.Prerender,
headers: { Location: 'https://example.com/docs?from=ssg&next=/ssg' },
},
{
path: 'ssg/:id',
renderMode: RenderMode.Prerender,
Expand Down Expand Up @@ -115,6 +124,9 @@ export default async function () {
'ssg/two/index.html': /ng-server-context="ssg".+ssg-with-params works!/,
// When static redirects are generated as meta tags.
'ssg-redirect/index.html': '<meta http-equiv="refresh" content="0; url=/ssg">',
// The target of a 'Location' header is HTML escaped before it is written to the page.
'ssg-redirect-external/index.html':
'<meta http-equiv="refresh" content="0; url=https://example.com/docs?from=ssg&amp;next=/ssg">',
'ssg-redirect-via-guard/index.html':
'<meta http-equiv="refresh" content="0; url=/ssg?foo=bar">',
};
Expand Down
Loading