diff --git a/tracker/tracker/package-lock.json b/tracker/tracker/package-lock.json index 2d6f47ed9..6eba67e3c 100644 --- a/tracker/tracker/package-lock.json +++ b/tracker/tracker/package-lock.json @@ -1,6 +1,6 @@ { "name": "@openreplay/tracker", - "version": "3.2.5", + "version": "3.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/tracker/tracker/package.json b/tracker/tracker/package.json index c6049da67..d88d366da 100644 --- a/tracker/tracker/package.json +++ b/tracker/tracker/package.json @@ -1,7 +1,7 @@ { "name": "@openreplay/tracker", "description": "The OpenReplay tracker main package", - "version": "3.3.0", + "version": "3.4.0", "keywords": [ "logging", "replay" diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts index 8102df880..5efea1cf9 100644 --- a/tracker/tracker/src/main/app/index.ts +++ b/tracker/tracker/src/main/app/index.ts @@ -1,4 +1,4 @@ -import { timestamp, log } from '../utils'; +import { timestamp, log, warn } from '../utils'; import { Timestamp, TechnicalInfo, PageClose } from '../../messages'; import Message from '../../messages/message'; import Nodes from './nodes'; @@ -24,6 +24,8 @@ export type Options = { session_pageno_key: string; local_uuid_key: string; ingestPoint: string; + resourceBaseHref: string, // resourceHref? + //resourceURLRewriter: (url: string) => string | boolean, __is_snippet: boolean; __debug_report_edp: string | null; onStart?: (info: OnStartInfo) => void; @@ -65,10 +67,12 @@ export default class App { session_pageno_key: '__openreplay_pageno', local_uuid_key: '__openreplay_uuid', ingestPoint: DEFAULT_INGEST_POINT, + resourceBaseHref: '', __is_snippet: false, __debug_report_edp: null, obscureTextEmails: true, obscureTextNumbers: false, + captureIFrames: false, }, opts, ); @@ -118,6 +122,7 @@ export default class App { if(this.options.__debug_report_edp !== null) { fetch(this.options.__debug_report_edp, { method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ context, error: `${e}` @@ -199,11 +204,26 @@ export default class App { return this._sessionID || undefined; } getHost(): string { - return new URL(this.options.ingestPoint).host; + return new URL(this.options.ingestPoint).hostname + } + getProjectKey(): string { + return this.projectKey + } + getBaseHref(): string { + if (this.options.resourceBaseHref) { + return this.options.resourceBaseHref + } + if (document.baseURI) { + return document.baseURI + } + // IE only + return document.head + ?.getElementsByTagName("base")[0] + ?.getAttribute("href") || location.origin + location.pathname } isServiceURL(url: string): boolean { - return url.startsWith(this.options.ingestPoint); + return url.startsWith(this.options.ingestPoint) } active(): boolean { @@ -211,10 +231,10 @@ export default class App { } private _start(reset: boolean): Promise { if (!this.isActive) { - this.isActive = true; if (!this.worker) { - throw new Error("Stranger things: no worker found"); + return Promise.reject("No worker found: perhaps, CSP is not set."); } + this.isActive = true; let pageNo: number = 0; const pageNoStr = sessionStorage.getItem(this.options.session_pageno_key); @@ -273,7 +293,7 @@ export default class App { this._sessionID = sessionID; } if (!this.worker) { - throw new Error("Stranger things: no worker found after start request"); + throw new Error("no worker found after start request (this might not happen)"); } this.worker.postMessage({ token, beaconSizeLimit }); this.startCallbacks.forEach((cb) => cb()); @@ -289,6 +309,7 @@ export default class App { }) .catch(e => { this.stop(); + warn("OpenReplay was unable to start. ", e) this.sendDebugReport("session_start", e); throw e; }) diff --git a/tracker/tracker/src/main/app/observer.ts b/tracker/tracker/src/main/app/observer.ts index 88168cad8..493c7aaac 100644 --- a/tracker/tracker/src/main/app/observer.ts +++ b/tracker/tracker/src/main/app/observer.ts @@ -1,4 +1,4 @@ -import { stars, hasOpenreplayAttribute, getBaseURI } from '../utils'; +import { stars, hasOpenreplayAttribute } from '../utils'; import { CreateDocument, CreateElementNode, @@ -10,37 +10,49 @@ import { RemoveNodeAttribute, MoveNode, RemoveNode, + CreateIFrameDocument, } from '../../messages'; import App from './index'; +interface Window extends WindowProxy { + HTMLInputElement: typeof HTMLInputElement, + HTMLLinkElement: typeof HTMLLinkElement, + HTMLStyleElement: typeof HTMLStyleElement, + SVGStyleElement: typeof SVGStyleElement, + HTMLIFrameElement: typeof HTMLIFrameElement, + Text: typeof Text, + Element: typeof Element, + //parent: Window, +} + + +type WindowConstructor = + Document | + Element | + Text | + HTMLInputElement | + HTMLLinkElement | + HTMLStyleElement | + HTMLIFrameElement + +// type ConstructorNames = +// 'Element' | +// 'Text' | +// 'HTMLInputElement' | +// 'HTMLLinkElement' | +// 'HTMLStyleElement' | +// 'HTMLIFrameElement' +type Constructor = { new (...args: any[]): T , name: string }; + + function isSVGElement(node: Element): node is SVGElement { return node.namespaceURI === 'http://www.w3.org/2000/svg'; } -function isIgnored(node: Node): boolean { - if (node instanceof Text) { - return false; - } - if (!(node instanceof Element)) { - return true; - } - const tag = node.tagName.toUpperCase(); - if (tag === 'LINK') { - const rel = node.getAttribute('rel'); - const as = node.getAttribute('as'); - return !(rel?.includes('stylesheet') || as === "style" || as === "font"); - } - return ( - tag === 'SCRIPT' || - tag === 'NOSCRIPT' || - tag === 'META' || - tag === 'TITLE' || - tag === 'BASE' - ); -} export interface Options { obscureTextEmails: boolean; obscureTextNumbers: boolean; + captureIFrames: boolean; } export default class Observer { @@ -51,17 +63,33 @@ export default class Observer { private readonly attributesList: Array | undefined>; private readonly textSet: Set; private readonly textMasked: Set; - private readonly options: Options; - constructor(private readonly app: App, opts: Options) { - this.options = opts; + constructor(private readonly app: App, private readonly options: Options, private readonly context: Window = window) { this.observer = new MutationObserver( this.app.safe((mutations) => { for (const mutation of mutations) { const target = mutation.target; - if (isIgnored(target) || !document.contains(target)) { + const type = mutation.type; + + // Special case + // Document 'childList' might happen in case of iframe. + // TODO: generalize as much as possible + if (this.isInstance(target, Document) + && type === 'childList' + //&& new Array(mutation.addedNodes).some(node => this.isInstance(node, HTMLHtmlElement)) + ) { + const parentFrame = target.defaultView?.frameElement + if (!parentFrame) { continue } + this.bindTree(target.documentElement) + const frameID = this.app.nodes.getID(parentFrame) + const docID = this.app.nodes.getID(target.documentElement) + if (frameID === undefined || docID === undefined) { continue } + this.app.send(CreateIFrameDocument(frameID, docID)); + continue; + } + + if (this.isIgnored(target) || !context.document.contains(target)) { continue; } - const type = mutation.type; if (type === 'childList') { for (let i = 0; i < mutation.removedNodes.length; i++) { this.bindTree(mutation.removedNodes[i]); @@ -114,6 +142,43 @@ export default class Observer { this.textMasked.clear(); } + // TODO: we need a type expert here so we won't have to ignore the lines + private isInstance(node: Node, constr: Constructor): node is T { + let context = this.context; + while(context.parent && context.parent !== context) { + // @ts-ignore + if (node instanceof context[constr.name]) { + return true + } + // @ts-ignore + context = context.parent + } + // @ts-ignore + return node instanceof context[constr.name] + } + + private isIgnored(node: Node): boolean { + if (this.isInstance(node, Text)) { + return false; + } + if (!this.isInstance(node, Element)) { + return true; + } + const tag = node.tagName.toUpperCase(); + if (tag === 'LINK') { + const rel = node.getAttribute('rel'); + const as = node.getAttribute('as'); + return !(rel?.includes('stylesheet') || as === "style" || as === "font"); + } + return ( + tag === 'SCRIPT' || + tag === 'NOSCRIPT' || + tag === 'META' || + tag === 'TITLE' || + tag === 'BASE' + ); + } + private sendNodeAttribute( id: number, node: Element, @@ -130,7 +195,7 @@ export default class Observer { if (value.length > 1e5) { value = ''; } - this.app.send(new SetNodeAttributeURLBased(id, name, value, getBaseURI())); + this.app.send(new SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref())); } else { this.app.send(new SetNodeAttribute(id, name, value)); } @@ -148,7 +213,7 @@ export default class Observer { } if ( name === 'value' && - node instanceof HTMLInputElement && + this.isInstance(node, HTMLInputElement) && node.type !== 'button' && node.type !== 'reset' && node.type !== 'submit' @@ -159,8 +224,8 @@ export default class Observer { this.app.send(new RemoveNodeAttribute(id, name)); return; } - if (name === 'style' || name === 'href' && node instanceof HTMLLinkElement) { - this.app.send(new SetNodeAttributeURLBased(id, name, value, getBaseURI())); + if (name === 'style' || name === 'href' && this.isInstance(node, HTMLLinkElement)) { + this.app.send(new SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref())); return; } if (name === 'href' || value.length > 1e5) { @@ -170,8 +235,8 @@ export default class Observer { } private sendNodeData(id: number, parentElement: Element, data: string): void { - if (parentElement instanceof HTMLStyleElement || parentElement instanceof SVGStyleElement) { - this.app.send(new SetCSSDataURLBased(id, data, getBaseURI())); + if (this.isInstance(parentElement, HTMLStyleElement) || this.isInstance(parentElement, SVGStyleElement)) { + this.app.send(new SetCSSDataURLBased(id, data, this.app.getBaseHref())); return; } if (this.textMasked.has(id)) { @@ -201,7 +266,7 @@ export default class Observer { } private bindTree(node: Node): void { - if (isIgnored(node)) { + if (this.isIgnored(node)) { return; } this.bindNode(node); @@ -210,7 +275,7 @@ export default class Observer { NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, { acceptNode: (node) => - isIgnored(node) || this.app.nodes.getID(node) !== undefined + this.isIgnored(node) || this.app.nodes.getID(node) !== undefined ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT, }, @@ -231,7 +296,9 @@ export default class Observer { private _commitNode(id: number, node: Node): boolean { const parent = node.parentNode; let parentID: number | undefined; - if (id !== 0) { + if (this.isInstance(node, HTMLHtmlElement)) { + this.indexes[id] = 0 + } else { if (parent === null) { this.unbindNode(node); return false; @@ -247,7 +314,7 @@ export default class Observer { } if ( this.textMasked.has(parentID) || - (node instanceof Element && hasOpenreplayAttribute(node, 'masked')) + (this.isInstance(node, Element) && hasOpenreplayAttribute(node, 'masked')) ) { this.textMasked.add(id); } @@ -271,7 +338,7 @@ export default class Observer { throw 'commitNode: missing node index'; } if (isNew === true) { - if (node instanceof Element) { + if (this.isInstance(node, Element)) { if (parentID !== undefined) { this.app.send(new CreateElementNode( @@ -287,7 +354,12 @@ export default class Observer { const attr = node.attributes[i]; this.sendNodeAttribute(id, node, attr.nodeName, attr.value); } - } else if (node instanceof Text) { + + if (this.isInstance(node, HTMLIFrameElement) && + (this.options.captureIFrames || node.getAttribute("data-openreplay-capture"))) { + this.handleIframe(node); + } + } else if (this.isInstance(node, Text)) { // for text node id != 0, hence parentID !== undefined and parent is Element this.app.send(new CreateTextNode(id, parentID as number, index)); this.sendNodeData(id, parent as Element, node.data); @@ -299,7 +371,7 @@ export default class Observer { } const attr = this.attributesList[id]; if (attr !== undefined) { - if (!(node instanceof Element)) { + if (!this.isInstance(node, Element)) { throw 'commitNode: node is not an element'; } for (const name of attr) { @@ -307,7 +379,7 @@ export default class Observer { } } if (this.textSet.has(id)) { - if (!(node instanceof Text)) { + if (!this.isInstance(node, Text)) { throw 'commitNode: node is not a text'; } // for text node id != 0, hence parent is Element @@ -337,8 +409,44 @@ export default class Observer { this.clear(); } + private iframeObservers: Observer[] = []; + private handleIframe(iframe: HTMLIFrameElement): void { + const handle = () => { + const context = iframe.contentWindow as Window | null + const id = this.app.nodes.getID(iframe) + if (!context || id === undefined) { return } + + const observer = new Observer(this.app, this.options, context) + this.iframeObservers.push(observer) + observer.observeIframe(id, context) + } + this.app.attachEventListener(iframe, "load", handle) + handle() + } + + // TODO: abstract common functionality, separate FrameObserver + private observeIframe(id: number, context: Window) { + const doc = context.document; + this.observer.observe(doc, { + childList: true, + attributes: true, + characterData: true, + subtree: true, + attributeOldValue: false, + characterDataOldValue: false, + }); + this.bindTree(doc.documentElement); + const docID = this.app.nodes.getID(doc.documentElement); + if (docID === undefined) { + console.log("Wrong") + return; + } + this.app.send(CreateIFrameDocument(id,docID)); + this.commitNodes(); + } + observe(): void { - this.observer.observe(document, { + this.observer.observe(this.context.document, { childList: true, attributes: true, characterData: true, @@ -347,11 +455,13 @@ export default class Observer { characterDataOldValue: false, }); this.app.send(new CreateDocument()); - this.bindTree(document.documentElement); + this.bindTree(this.context.document.documentElement); this.commitNodes(); } disconnect(): void { + this.iframeObservers.forEach(o => o.disconnect()); + this.iframeObservers = []; this.observer.disconnect(); this.clear(); } diff --git a/tracker/tracker/src/main/modules/cssrules.ts b/tracker/tracker/src/main/modules/cssrules.ts index 366a7d3fe..54166f717 100644 --- a/tracker/tracker/src/main/modules/cssrules.ts +++ b/tracker/tracker/src/main/modules/cssrules.ts @@ -1,6 +1,5 @@ import App from '../app'; import { CSSInsertRuleURLBased, CSSDeleteRule, TechnicalInfo } from '../../messages'; -import { getBaseURI } from '../utils'; export default function(app: App | null) { if (app === null) { @@ -14,7 +13,7 @@ export default function(app: App | null) { const processOperation = app.safe( (stylesheet: CSSStyleSheet, index: number, rule?: string) => { const sendMessage = typeof rule === 'string' - ? (nodeID: number) => app.send(new CSSInsertRuleURLBased(nodeID, rule, index, getBaseURI())) + ? (nodeID: number) => app.send(new CSSInsertRuleURLBased(nodeID, rule, index, app.getBaseHref())) : (nodeID: number) => app.send(new CSSDeleteRule(nodeID, index)); // TODO: Extend messages to maintain nested rules (CSSGroupingRule prototype, as well as CSSKeyframesRule) if (stylesheet.ownerNode == null) { diff --git a/tracker/tracker/src/main/modules/img.ts b/tracker/tracker/src/main/modules/img.ts index d7d4a6be5..e20a4d531 100644 --- a/tracker/tracker/src/main/modules/img.ts +++ b/tracker/tracker/src/main/modules/img.ts @@ -1,4 +1,4 @@ -import { timestamp, isURL, getBaseURI } from '../utils'; +import { timestamp, isURL } from '../utils'; import App from '../app'; import { ResourceTiming, SetNodeAttributeURLBased } from '../../messages'; @@ -17,7 +17,7 @@ export default function (app: App): void { app.send(new ResourceTiming(timestamp(), 0, 0, 0, 0, 0, src, 'img')); } } else if (src.length < 1e5) { - app.send(new SetNodeAttributeURLBased(id, 'src', src, getBaseURI())); + app.send(new SetNodeAttributeURLBased(id, 'src', src, app.getBaseHref())); } }); @@ -30,7 +30,7 @@ export default function (app: App): void { return; } const src = target.src; - app.send(new SetNodeAttributeURLBased(id, 'src', src, getBaseURI())); + app.send(new SetNodeAttributeURLBased(id, 'src', src, app.getBaseHref())); } } }); diff --git a/tracker/tracker/src/main/modules/mouse.ts b/tracker/tracker/src/main/modules/mouse.ts index 96b973e14..a0a526b93 100644 --- a/tracker/tracker/src/main/modules/mouse.ts +++ b/tracker/tracker/src/main/modules/mouse.ts @@ -72,14 +72,23 @@ function getTargetLabel(target: Element): string { return ''; } +interface HeatmapsOptions { + finder: FinderOptions, +} + export interface Options { - selectorFinder: boolean | FinderOptions; + heatmaps: boolean | HeatmapsOptions; } export default function (app: App, opts: Partial): void { const options: Options = Object.assign( { - selectorFinder: true, + heatmaps: { + finder: { + threshold: 5, + maxNumberOfTries: 600, + }, + }, }, opts, ); @@ -106,9 +115,9 @@ export default function (app: App, opts: Partial): void { const selectorMap: {[id:number]: string} = {}; function getSelector(id: number, target: Element): string { - if (options.selectorFinder === false) { return '' } + if (options.heatmaps === false) { return '' } return selectorMap[id] = selectorMap[id] || - finder(target, options.selectorFinder === true ? undefined : options.selectorFinder); + finder(target, options.heatmaps === true ? undefined : options.heatmaps.finder); } app.attachEventListener( diff --git a/tracker/tracker/src/main/utils.ts b/tracker/tracker/src/main/utils.ts index 350447b62..586d02ecb 100644 --- a/tracker/tracker/src/main/utils.ts +++ b/tracker/tracker/src/main/utils.ts @@ -16,16 +16,6 @@ export function isURL(s: string): boolean { return s.substr(0, 8) === 'https://' || s.substr(0, 7) === 'http://'; } -export function getBaseURI(): string { - if (document.baseURI) { - return document.baseURI; - } - // IE only - return document.head - ?.getElementsByTagName("base")[0] - ?.getAttribute("href") || location.origin + location.pathname; -} - export const IN_BROWSER = !(typeof window === "undefined"); export const log = console.log diff --git a/tracker/tracker/src/main/vendors/finder/finder.d.ts b/tracker/tracker/src/main/vendors/finder/finder.d.ts deleted file mode 100644 index aaff849fb..000000000 --- a/tracker/tracker/src/main/vendors/finder/finder.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -export declare type Options = { - root: Element; - idName: (name: string) => boolean; - className: (name: string) => boolean; - tagName: (name: string) => boolean; - attr: (name: string, value: string) => boolean; - seedMinLength: number; - optimizedMinLength: number; - threshold: number; - maxNumberOfTries: number; -}; -export declare function finder(input: Element, options?: Partial): string; diff --git a/tracker/tracker/src/main/vendors/finder/finder.js b/tracker/tracker/src/main/vendors/finder/finder.js deleted file mode 100644 index 0e5eab2d7..000000000 --- a/tracker/tracker/src/main/vendors/finder/finder.js +++ /dev/null @@ -1,339 +0,0 @@ -var Limit; -(function (Limit) { - Limit[Limit["All"] = 0] = "All"; - Limit[Limit["Two"] = 1] = "Two"; - Limit[Limit["One"] = 2] = "One"; -})(Limit || (Limit = {})); -let config; -let rootDocument; -export function finder(input, options) { - if (input.nodeType !== Node.ELEMENT_NODE) { - throw new Error(`Can't generate CSS selector for non-element node type.`); - } - if ("html" === input.tagName.toLowerCase()) { - return "html"; - } - const defaults = { - root: document.body, - idName: (name) => true, - className: (name) => true, - tagName: (name) => true, - attr: (name, value) => false, - seedMinLength: 1, - optimizedMinLength: 2, - threshold: 1000, - maxNumberOfTries: 10000, - }; - config = Object.assign(Object.assign({}, defaults), options); - rootDocument = findRootDocument(config.root, defaults); - let path = bottomUpSearch(input, Limit.All, () => bottomUpSearch(input, Limit.Two, () => bottomUpSearch(input, Limit.One))); - if (path) { - const optimized = sort(optimize(path, input)); - if (optimized.length > 0) { - path = optimized[0]; - } - return selector(path); - } - else { - throw new Error(`Selector was not found.`); - } -} -function findRootDocument(rootNode, defaults) { - if (rootNode.nodeType === Node.DOCUMENT_NODE) { - return rootNode; - } - if (rootNode === defaults.root) { - return rootNode.ownerDocument; - } - return rootNode; -} -function bottomUpSearch(input, limit, fallback) { - let path = null; - let stack = []; - let current = input; - let i = 0; - while (current && current !== config.root.parentElement) { - let level = maybe(id(current)) || maybe(...attr(current)) || maybe(...classNames(current)) || maybe(tagName(current)) || [any()]; - const nth = index(current); - if (limit === Limit.All) { - if (nth) { - level = level.concat(level.filter(dispensableNth).map(node => nthChild(node, nth))); - } - } - else if (limit === Limit.Two) { - level = level.slice(0, 1); - if (nth) { - level = level.concat(level.filter(dispensableNth).map(node => nthChild(node, nth))); - } - } - else if (limit === Limit.One) { - const [node] = level = level.slice(0, 1); - if (nth && dispensableNth(node)) { - level = [nthChild(node, nth)]; - } - } - for (let node of level) { - node.level = i; - } - stack.push(level); - if (stack.length >= config.seedMinLength) { - path = findUniquePath(stack, fallback); - if (path) { - break; - } - } - current = current.parentElement; - i++; - } - if (!path) { - path = findUniquePath(stack, fallback); - } - return path; -} -function findUniquePath(stack, fallback) { - const paths = sort(combinations(stack)); - if (paths.length > config.threshold) { - return fallback ? fallback() : null; - } - for (let candidate of paths) { - if (unique(candidate)) { - return candidate; - } - } - return null; -} -function selector(path) { - let node = path[0]; - let query = node.name; - for (let i = 1; i < path.length; i++) { - const level = path[i].level || 0; - if (node.level === level - 1) { - query = `${path[i].name} > ${query}`; - } - else { - query = `${path[i].name} ${query}`; - } - node = path[i]; - } - return query; -} -function penalty(path) { - return path.map(node => node.penalty).reduce((acc, i) => acc + i, 0); -} -function unique(path) { - switch (rootDocument.querySelectorAll(selector(path)).length) { - case 0: - throw new Error(`Can't select any node with this selector: ${selector(path)}`); - case 1: - return true; - default: - return false; - } -} -function id(input) { - const elementId = input.getAttribute("id"); - if (elementId && config.idName(elementId)) { - return { - name: "#" + cssesc(elementId, { isIdentifier: true }), - penalty: 0, - }; - } - return null; -} -function attr(input) { - const attrs = Array.from(input.attributes).filter((attr) => config.attr(attr.name, attr.value)); - return attrs.map((attr) => ({ - name: "[" + cssesc(attr.name, { isIdentifier: true }) + "=\"" + cssesc(attr.value) + "\"]", - penalty: 0.5 - })); -} -function classNames(input) { - const names = Array.from(input.classList) - .filter(config.className); - return names.map((name) => ({ - name: "." + cssesc(name, { isIdentifier: true }), - penalty: 1 - })); -} -function tagName(input) { - const name = input.tagName.toLowerCase(); - if (config.tagName(name)) { - return { - name, - penalty: 2 - }; - } - return null; -} -function any() { - return { - name: "*", - penalty: 3 - }; -} -function index(input) { - const parent = input.parentNode; - if (!parent) { - return null; - } - let child = parent.firstChild; - if (!child) { - return null; - } - let i = 0; - while (child) { - if (child.nodeType === Node.ELEMENT_NODE) { - i++; - } - if (child === input) { - break; - } - child = child.nextSibling; - } - return i; -} -function nthChild(node, i) { - return { - name: node.name + `:nth-child(${i})`, - penalty: node.penalty + 1 - }; -} -function dispensableNth(node) { - return node.name !== "html" && !node.name.startsWith("#"); -} -function maybe(...level) { - const list = level.filter(notEmpty); - if (list.length > 0) { - return list; - } - return null; -} -function notEmpty(value) { - return value !== null && value !== undefined; -} -function* combinations(stack, path = []) { - if (stack.length > 0) { - for (let node of stack[0]) { - yield* combinations(stack.slice(1, stack.length), path.concat(node)); - } - } - else { - yield path; - } -} -function sort(paths) { - return Array.from(paths).sort((a, b) => penalty(a) - penalty(b)); -} -function* optimize(path, input, scope = { - counter: 0, - visited: new Map() -}) { - if (path.length > 2 && path.length > config.optimizedMinLength) { - for (let i = 1; i < path.length - 1; i++) { - if (scope.counter > config.maxNumberOfTries) { - return; // Okay At least I tried! - } - scope.counter += 1; - const newPath = [...path]; - newPath.splice(i, 1); - const newPathKey = selector(newPath); - if (scope.visited.has(newPathKey)) { - return; - } - if (unique(newPath) && same(newPath, input)) { - yield newPath; - scope.visited.set(newPathKey, true); - yield* optimize(newPath, input, scope); - } - } - } -} -function same(path, input) { - return rootDocument.querySelector(selector(path)) === input; -} -const regexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/; -const regexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/; -const regexExcessiveSpaces = /(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g; -const defaultOptions = { - "escapeEverything": false, - "isIdentifier": false, - "quotes": "single", - "wrap": false -}; -function cssesc(string, opt = {}) { - const options = Object.assign(Object.assign({}, defaultOptions), opt); - if (options.quotes != "single" && options.quotes != "double") { - options.quotes = "single"; - } - const quote = options.quotes == "double" ? "\"" : "'"; - const isIdentifier = options.isIdentifier; - const firstChar = string.charAt(0); - let output = ""; - let counter = 0; - const length = string.length; - while (counter < length) { - const character = string.charAt(counter++); - let codePoint = character.charCodeAt(0); - let value = void 0; - // If it’s not a printable ASCII character… - if (codePoint < 0x20 || codePoint > 0x7E) { - if (codePoint >= 0xD800 && codePoint <= 0xDBFF && counter < length) { - // It’s a high surrogate, and there is a next character. - const extra = string.charCodeAt(counter++); - if ((extra & 0xFC00) == 0xDC00) { - // next character is low surrogate - codePoint = ((codePoint & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000; - } - else { - // It’s an unmatched surrogate; only append this code unit, in case - // the next code unit is the high surrogate of a surrogate pair. - counter--; - } - } - value = "\\" + codePoint.toString(16).toUpperCase() + " "; - } - else { - if (options.escapeEverything) { - if (regexAnySingleEscape.test(character)) { - value = "\\" + character; - } - else { - value = "\\" + codePoint.toString(16).toUpperCase() + " "; - } - } - else if (/[\t\n\f\r\x0B]/.test(character)) { - value = "\\" + codePoint.toString(16).toUpperCase() + " "; - } - else if (character == "\\" || !isIdentifier && (character == "\"" && quote == character || character == "'" && quote == character) || isIdentifier && regexSingleEscape.test(character)) { - value = "\\" + character; - } - else { - value = character; - } - } - output += value; - } - if (isIdentifier) { - if (/^-[-\d]/.test(output)) { - output = "\\-" + output.slice(1); - } - else if (/\d/.test(firstChar)) { - output = "\\3" + firstChar + " " + output.slice(1); - } - } - // Remove spaces after `\HEX` escapes that are not followed by a hex digit, - // since they’re redundant. Note that this is only possible if the escape - // sequence isn’t preceded by an odd number of backslashes. - output = output.replace(regexExcessiveSpaces, function ($0, $1, $2) { - if ($1 && $1.length % 2) { - // It’s not safe to remove the space, so don’t. - return $0; - } - // Strip the space. - return ($1 || "") + $2; - }); - if (!isIdentifier && options.wrap) { - return quote + output + quote; - } - return output; -} diff --git a/tracker/tracker/src/main/vendors/finder/finder.ts b/tracker/tracker/src/main/vendors/finder/finder.ts index bb2621d75..fc9f64af2 100644 --- a/tracker/tracker/src/main/vendors/finder/finder.ts +++ b/tracker/tracker/src/main/vendors/finder/finder.ts @@ -279,14 +279,16 @@ function notEmpty(value: T | null | undefined): value is T { return value !== null && value !== undefined } -function* combinations(stack: Node[][], path: Node[] = []): Generator { +function combinations(stack: Node[][], path: Node[] = []): Node[][] { + const paths: Node[][] = [] if (stack.length > 0) { for (let node of stack[0]) { - yield* combinations(stack.slice(1, stack.length), path.concat(node)) + paths.push(...combinations(stack.slice(1, stack.length), path.concat(node))) } } else { - yield path + paths.push(path) } + return paths } function sort(paths: Iterable): Path[] { @@ -298,29 +300,31 @@ type Scope = { visited: Map } -function* optimize(path: Path, input: Element, scope: Scope = { +function optimize(path: Path, input: Element, scope: Scope = { counter: 0, visited: new Map() -}): Generator { +}): Node[][] { + const paths: Node[][] = [] if (path.length > 2 && path.length > config.optimizedMinLength) { for (let i = 1; i < path.length - 1; i++) { if (scope.counter > config.maxNumberOfTries) { - return // Okay At least I tried! + return paths // Okay At least I tried! } scope.counter += 1 const newPath = [...path] newPath.splice(i, 1) const newPathKey = selector(newPath) if (scope.visited.has(newPathKey)) { - return + return paths } if (unique(newPath) && same(newPath, input)) { - yield newPath + paths.push(newPath) scope.visited.set(newPathKey, true) - yield* optimize(newPath, input, scope) + paths.push(...optimize(newPath, input, scope)) } } } + return paths } function same(path: Path, input: Element) { diff --git a/tracker/tracker/src/messages/index.ts b/tracker/tracker/src/messages/index.ts index 487868202..210f534cb 100644 --- a/tracker/tracker/src/messages/index.ts +++ b/tracker/tracker/src/messages/index.ts @@ -885,3 +885,19 @@ export const MouseClick = bindNew(_MouseClick); classes.set(69, MouseClick); +class _CreateIFrameDocument implements Message { + readonly _id: number = 70; + constructor( + public frameID: number, + public id: number + ) {} + encode(writer: Writer): boolean { + return writer.uint(70) && + writer.uint(this.frameID) && + writer.uint(this.id); + } +} +export const CreateIFrameDocument = bindNew(_CreateIFrameDocument); +classes.set(70, CreateIFrameDocument); + +