import { captureError } from '../errors' import { lieProps, PHANTOM_DARKNESS, documentLie } from '../lies' import { instanceId, hashMini } from '../utils/crypto' import { createTimer, queueEvent, EMOJIS, CSS_FONT_FAMILY, IS_BLINK, IS_GECKO, logTestResult, performanceLogger, hashSlice, formatEmojiSet } from '../utils/helpers' import { patch, html, HTMLNote, getDiffs } from '../utils/html' function getRectSum(rect: Record): number { return Object.keys(rect).reduce((acc, key) => acc += rect[key], 0)/100_000_000 } // inspired by // https://privacycheck.sec.lrz.de/active/fp_gcr/fp_getclientrects.html // https://privacycheck.sec.lrz.de/active/fp_e/fp_emoji.html export default async function getClientRects() { try { const timer = createTimer() await queueEvent(timer) const toNativeObject = (domRect: DOMRect): Record => { return { bottom: domRect.bottom, height: domRect.height, left: domRect.left, right: domRect.right, width: domRect.width, top: domRect.top, x: domRect.x, y: domRect.y, } } let lied = ( lieProps['Element.getClientRects'] || lieProps['Element.getBoundingClientRect'] || lieProps['Range.getClientRects'] || lieProps['Range.getBoundingClientRect'] || lieProps['String.fromCodePoint'] ) || false const DOC = ( PHANTOM_DARKNESS && PHANTOM_DARKNESS.document && PHANTOM_DARKNESS.document.body ? PHANTOM_DARKNESS.document : document ) const getBestRect = (el: Element) => { let range if (!lieProps['Element.getClientRects']) { return el.getClientRects()[0] } else if (!lieProps['Element.getBoundingClientRect']) { return el.getBoundingClientRect() } else if (!lieProps['Range.getClientRects']) { range = DOC.createRange() range.selectNode(el) return range.getClientRects()[0] } range = DOC.createRange() range.selectNode(el) return range.getBoundingClientRect() } const rectsId = `${instanceId}-client-rects-div` const divElement = document.createElement('div') divElement.setAttribute('id', rectsId) DOC.body.appendChild(divElement) patch(divElement, html`
${ EMOJIS.map((emoji) => { return `
${emoji}
` }).join('') }
`) // get emoji set and system const pattern: Set = new Set() await queueEvent(timer) const emojiElems = [...DOC.getElementsByClassName('domrect-emoji')] const emojiSet = emojiElems.reduce((emojiSet, el, i) => { const emoji = EMOJIS[i] const { height, width } = getBestRect(el) const dimensions = `${width},${height}` if (!pattern.has(dimensions)) { pattern.add(dimensions) emojiSet.add(emoji) } return emojiSet }, new Set() as Set) const domrectSystemSum = 0.00001 * [...pattern].map((x) => { return x.split(',').reduce((acc, x) => acc += (+x||0), 0) }).reduce((acc, x) => acc += x, 0) // get clientRects const range = document.createRange() const rectElems = DOC.getElementsByClassName('rects') const elementClientRects = [...rectElems].map((el) => { return toNativeObject(el.getClientRects()[0]) }) const elementBoundingClientRect = [...rectElems].map((el) => { return toNativeObject(el.getBoundingClientRect()) }) const rangeClientRects = [...rectElems].map((el) => { range.selectNode(el) return toNativeObject(range.getClientRects()[0]) }) const rangeBoundingClientRect = [...rectElems].map((el) => { range.selectNode(el) return toNativeObject(el.getBoundingClientRect()) }) // detect failed shift calculation // inspired by https://arkenfox.github.io/TZP const rect4 = [...rectElems][3] const { top: initialTop } = elementClientRects[3] rect4.classList.add('shift-dom-rect') const { top: shiftedTop } = toNativeObject(rect4.getClientRects()[0]) rect4.classList.remove('shift-dom-rect') const { top: unshiftedTop } = toNativeObject(rect4.getClientRects()[0]) const diff = initialTop - shiftedTop const unshiftLie = diff != (unshiftedTop - shiftedTop) if (unshiftLie) { lied = true documentLie('Element.getClientRects', 'failed unshift calculation') } // detect failed math calculation lie let mathLie = false elementClientRects.forEach((rect) => { const { right, left, width, bottom, top, height, x, y } = rect if ( right - left != width || bottom - top != height || right - x != width || bottom - y != height ) { lied = true mathLie = true } return }) if (mathLie) { documentLie('Element.getClientRects', 'failed math calculation') } // detect equal elements mismatch lie const { right: right1, left: left1 } = elementClientRects[10] const { right: right2, left: left2 } = elementClientRects[11] if (right1 != right2 || left1 != left2) { documentLie('Element.getClientRects', 'equal elements mismatch') lied = true } // detect unknown rotate dimensions const knownEl = [...DOC.getElementsByClassName('rect-known')][0] const knownDimensions = toNativeObject(knownEl.getClientRects()[0]) const knownHash = hashMini(knownDimensions) if (IS_BLINK) { const Rotate: Record = { '9d9215cc': true, // 100, etc '47ded322': true, // 33, 67 'd0eceaa8': true, // 90 } if (!Rotate[knownHash]) { documentLie('Element.getClientRects', 'unknown rotate dimensions') lied = true } } else if (IS_GECKO) { const Rotate: Record = { 'e38453f0': true, // 100, etc } if (!Rotate[knownHash]) { documentLie('Element.getClientRects', 'unknown rotate dimensions') lied = true } } // detect ghost dimensions const ghostEl = [...DOC.getElementsByClassName('rect-ghost')][0] const ghostDimensions = toNativeObject(ghostEl.getClientRects()[0]) const hasGhostDimensions = Object.keys(ghostDimensions) .some((key) => ghostDimensions[key] !== 0) if (hasGhostDimensions) { documentLie('Element.getClientRects', 'unknown ghost dimensions') lied = true } DOC.body.removeChild(DOC.getElementById(rectsId) as HTMLElement) logTestResult({ time: timer.stop(), test: 'rects', passed: true }) return { elementClientRects, elementBoundingClientRect, rangeClientRects, rangeBoundingClientRect, emojiSet: [...emojiSet], domrectSystemSum, lied, } } catch (error) { logTestResult({ test: 'rects', passed: false }) captureError(error) return } } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function clientRectsHTML(fp: any) { if (!fp.clientRects) { return `
DOMRect
elems A: ${HTMLNote.BLOCKED}
elems B: ${HTMLNote.BLOCKED}
range A: ${HTMLNote.BLOCKED}
range B: ${HTMLNote.BLOCKED}
${HTMLNote.BLOCKED}
` } const { clientRects: { $hash, elementClientRects, elementBoundingClientRect, rangeClientRects, rangeBoundingClientRect, emojiSet, domrectSystemSum, lied, }, } = fp const computeDiffs = (rects: Record[]) => { if (!rects || !rects.length) { return } const expectedSum = rects.reduce((acc, rect) => { const { right, left, width, bottom, top, height } = rect const expected = { width: right - left, height: bottom - top, right: left + width, left: right - width, bottom: top + height, top: bottom - height, x: right - width, y: bottom - height, } return acc += getRectSum(expected) }, 0) const actualSum = rects.reduce((acc, rect) => acc += getRectSum(rect), 0) return getDiffs({ stringA: actualSum, stringB: expectedSum, charDiff: true, decorate: (diff) => `${diff}`, }) } const helpTitle = `Element.getClientRects()\nhash: ${hashMini(emojiSet)}\n${emojiSet.map((x: string, i: number) => i && (i % 6 == 0) ? `${x}\n` : x).join('')}` return `
${performanceLogger.getLog().rects} DOMRect${hashSlice($hash)}
elems A: ${computeDiffs(elementClientRects)}
elems B: ${computeDiffs(elementBoundingClientRect)}
range A: ${computeDiffs(rangeClientRects)}
range B: ${computeDiffs(rangeBoundingClientRect)}
${domrectSystemSum || HTMLNote.UNSUPPORTED} ${formatEmojiSet(emojiSet)}
` }