Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REPLAY-1075: Resolve relative URLS to absolute in stylesheets #1792

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
62 changes: 61 additions & 1 deletion packages/rum/src/domain/record/serializationUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { isIE } from '@datadog/browser-core'
import { NodePrivacyLevel } from '../../constants'
import { getSerializedNodeId, hasSerializedNode, setSerializedNodeId, getElementInputValue } from './serializationUtils'
import {
getSerializedNodeId,
hasSerializedNode,
setSerializedNodeId,
getElementInputValue,
switchToAbsoluteUrl,
} from './serializationUtils'

describe('serialized Node storage in DOM Nodes', () => {
describe('hasSerializedNode', () => {
Expand Down Expand Up @@ -95,3 +101,57 @@ describe('getElementInputValue', () => {
})
})
})

describe('replace relative urls by absolute ones', () => {
const cssHref = 'https://site.web/app-name/static/assets/resource.min.css'
const resolvedPath = 'https://site.web/app-name/static/assets/fonts/fontawesome-webfont.eot'

describe('replace relative url by absolute one', () => {
N-Boutaib marked this conversation as resolved.
Show resolved Hide resolved
it('should replace url when wrapped with signle quote', () => {
N-Boutaib marked this conversation as resolved.
Show resolved Hide resolved
const cssText = "{ font-family: FontAwesome; src: url('./fonts/fontawesome-webfont.eot'); }"
expect(switchToAbsoluteUrl(cssText, cssHref)).toEqual(
`{ font-family: FontAwesome; src: url('${resolvedPath}'); }`
)
})
it('should replace url when wrapped with double quote', () => {
N-Boutaib marked this conversation as resolved.
Show resolved Hide resolved
const cssText = '{ font-family: FontAwesome; src: url("./fonts/fontawesome-webfont.eot"); }'
expect(switchToAbsoluteUrl(cssText, cssHref)).toEqual(
`{ font-family: FontAwesome; src: url("${resolvedPath}"); }`
)
})
it('should replace url when not wrapped by a double quote', () => {
N-Boutaib marked this conversation as resolved.
Show resolved Hide resolved
const cssText = '{ font-family: FontAwesome; src: url(./fonts/fontawesome-webfont.eot); }'

expect(switchToAbsoluteUrl(cssText, cssHref)).toEqual(`{ font-family: FontAwesome; src: url(${resolvedPath}); }`)
})

it('should replace url when not wrapped by a double quote', () => {
N-Boutaib marked this conversation as resolved.
Show resolved Hide resolved
const cssText = '{ font-family: FontAwesome; src: url(fonts/fontawesome-webfont.eot); }'

expect(switchToAbsoluteUrl(cssText, cssHref)).toEqual(`{ font-family: FontAwesome; src: url(${resolvedPath}); }`)
})
})
describe('do not replace url in css text', () => {
it('should not replace url if baseUrl is null', () => {
const cssText = '{ font-family: FontAwesome; src: url(./fonts/fontawesome-webfont.eot); }'

expect(switchToAbsoluteUrl(cssText, null)).toEqual(cssText)
})
it('should not replace url if path is empty', () => {
const cssText = '{ font-family: FontAwesome; src: url(); }'

expect(switchToAbsoluteUrl(cssText, cssHref)).toEqual(cssText)
})
it('should not replace url if already absolute', () => {
const cssText =
'{ font-family: FontAwesome; src: url(https://site.web/app-name/static/assets/fonts/fontawesome-webfont.eot); }'

expect(switchToAbsoluteUrl(cssText, cssHref)).toEqual(cssText)
})
it('should not replace url if data uri', () => {
const cssText = '{ font-family: FontAwesome; src: url(data://static/assets/fonts/fontawesome-webfont.eot); }'
N-Boutaib marked this conversation as resolved.
Show resolved Hide resolved

expect(switchToAbsoluteUrl(cssText, cssHref)).toEqual(cssText)
})
})
})
36 changes: 36 additions & 0 deletions packages/rum/src/domain/record/serializationUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildUrl } from '@datadog/browser-core'
import type { NodePrivacyLevel } from '../../constants'
import { CENSORED_STRING_MARK } from '../../constants'
import { shouldMaskNode } from './privacy'
Expand Down Expand Up @@ -69,3 +70,38 @@ export function getElementInputValue(element: Element, nodePrivacyLevel: NodePri

return value
}

export const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")([^"]*)"|([^)]*))\)/gm
export const ABSOLUTE_URL = /^[A-Za-z]+:|^\/\//
N-Boutaib marked this conversation as resolved.
Show resolved Hide resolved
export const DATA_URI = /^data:.*,/i
N-Boutaib marked this conversation as resolved.
Show resolved Hide resolved

export function switchToAbsoluteUrl(cssText: string, cssHref: string | null): string {
return cssText.replace(
URL_IN_CSS_REF,
(
matched: string,
singleQuote: string | undefined,
firstPathMatched: string | undefined,
doubleQuote: string | undefined,
secondPathMatched: string | undefined,
thirdPathMatched: string | undefined
) => {
const path = firstPathMatched || secondPathMatched || thirdPathMatched
N-Boutaib marked this conversation as resolved.
Show resolved Hide resolved

if (!cssHref || !path || ABSOLUTE_URL.test(path) || DATA_URI.test(path)) {
return matched
}

const quote = singleQuote || doubleQuote || ''
return `url(${quote}${makeUrlAbsolute(path, cssHref)}${quote})`
}
)
}

export function makeUrlAbsolute(url: string, baseUrl: string): string {
try {
return buildUrl(url.trim(), baseUrl).href
N-Boutaib marked this conversation as resolved.
Show resolved Hide resolved
} catch (_) {
return url
}
N-Boutaib marked this conversation as resolved.
Show resolved Hide resolved
}
9 changes: 7 additions & 2 deletions packages/rum/src/domain/record/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ import {
getNodeSelfPrivacyLevel,
MAX_ATTRIBUTE_VALUE_CHAR_LENGTH,
} from './privacy'
import { getSerializedNodeId, setSerializedNodeId, getElementInputValue } from './serializationUtils'
import {
getSerializedNodeId,
setSerializedNodeId,
getElementInputValue,
switchToAbsoluteUrl,
} from './serializationUtils'
import { forEach } from './utils'
import type { ElementsScrollPositions } from './elementsScrollPositions'

Expand Down Expand Up @@ -316,7 +321,7 @@ function getValidTagName(tagName: string): string {
function getCssRulesString(s: CSSStyleSheet): string | null {
try {
const rules = s.rules || s.cssRules
return rules ? Array.from(rules).map(getCssRuleString).join('') : null
return rules ? switchToAbsoluteUrl(Array.from(rules, getCssRuleString).join(''), s.href) : null
N-Boutaib marked this conversation as resolved.
Show resolved Hide resolved
} catch (error) {
return null
}
Expand Down