Path: blob/main/plugins/default-browser-emulator/test/DomExtractor.js
1029 views
// copied from double-agent. do not modify manually!12function DomExtractor(selfName, pageMeta = {}) {3const { saveToUrl, pageUrl, pageHost, pageName } = pageMeta;4const skipProps = [5'Fingerprint2',6'pageQueue',7'runDomExtractor',8'pageLoaded',9'axios',10'justAFunction',11];1213const skipValues = ['innerHTML', 'outerHTML', 'innerText', 'outerText'];1415const doNotInvoke = [16'print',17'alert',18'prompt',19'confirm',20'open',21'close',22'reload',23'assert',24'requestPermission',25'screenshot',26'pageLoaded',27'delete',28'clear',29'read',3031'start',32'stop',3334'write',35'writeln',36'replaceWith',37'remove',3839'self.history.back',40'self.history.forward',41'self.history.go',42'self.history.pushState',43'self.history.replaceState',4445'getUserMedia',46'requestFullscreen',47'webkitRequestFullScreen',48'webkitRequestFullscreen',49'getDisplayMedia',50].map(x => x.replace(/self\./g, `${selfName}.`));5152const doNotAccess = [53'self.CSSAnimation.prototype.timeline', // crashes Safari54'self.Animation.prototype.timeline', // crashes Safari55'self.CSSTransition.prototype.timeline', // crashes Safari56].map(x => x.replace(/self\./g, `${selfName}.`));5758const excludedInheritedKeys = ['name', 'length', 'constructor'];59const loadedObjects = new Map([[self, selfName]]);60const hierarchyNav = new Map();61const detached = {};6263async function extractPropsFromObject(obj, parentPath) {64let keys = [];65let symbols = [];66try {67for (let key of Object.getOwnPropertyNames(obj)) {68if (!keys.includes(key)) keys.push(key);69}70} catch (err) {}71try {72symbols = Object.getOwnPropertySymbols(obj);73for (let key of symbols) {74if (!keys.includes(key)) keys.push(key);75}76} catch (err) {}7778try {79for (let key in obj) {80if (!keys.includes(key)) keys.push(key);81}82} catch (err) {}8384const newObj = {85_$protos: await loadProtoHierarchy(obj, parentPath),86};87if (88parentPath.includes(`${selfName}.document.`) &&89!parentPath.includes(`${selfName}.document.documentElement`) &&90newObj._$protos.includes('HTMLElement.prototype')91) {92newObj._$skipped = 'SKIPPED ELEMENT';93return newObj;94}9596if (parentPath.includes('new()') && parentPath.endsWith('.ownerElement')) {97newObj._$skipped = 'SKIPPED ELEMENT';98return newObj;99}100101if (parentPath.split('.').length >= 8) {102newObj._$skipped = 'SKIPPED MAX DEPTH';103return newObj;104}105106const isNewObject = parentPath.includes('.new()');107if (isNewObject && newObj._$protos[0] === 'HTMLDocument.prototype') {108newObj._$skipped = 'SKIPPED DOCUMENT';109newObj._$type = 'HTMLDocument.prototype';110return newObj;111}112if (Object.isFrozen(obj)) newObj._$isFrozen = true;113if (Object.isSealed(obj)) newObj._$isSealed = true;114if (!newObj._$protos.length) delete newObj._$protos;115116const inheritedProps = [];117if (isNewObject) {118let proto = obj;119while (!!proto) {120proto = Object.getPrototypeOf(proto);121if (122!proto ||123proto === Object ||124proto === Object.prototype ||125proto === Function ||126proto === Function.prototype ||127proto === HTMLElement.prototype ||128proto === EventTarget.prototype129)130break;131for (const key of Object.getOwnPropertyNames(proto)) {132if (!keys.includes(key) && !excludedInheritedKeys.includes(key)) inheritedProps.push(key);133}134}135}136// TODO: re-enable inherited properties once we are on stable ground with chrome flags137// keys.push(...inheritedProps)138139for (const key of keys) {140if (skipProps.includes(key)) {141continue;142}143if (key === 'constructor') continue;144145const path = parentPath + '.' + String(key);146if (path.endsWith('_GLOBAL_HOOK__')) continue;147148const prop = '' + String(key);149150if (151path.startsWith(`${selfName}.document`) &&152typeof key === 'string' &&153(key.startsWith('child') ||154key.startsWith('first') ||155key.startsWith('last') ||156key.startsWith('next') ||157key.startsWith('prev') ||158key === 'textContent' ||159key === 'text')160) {161newObj[prop] = { _$type: 'dom', _$skipped: 'SKIPPED DOM' };162continue;163}164165if (path.startsWith(`${selfName}.document`) && path.split('.').length > 5) {166newObj[prop] = { _$type: 'object', _$skipped: 'SKIPPED DEPTH' };167continue;168}169170if (key === 'style') {171if (isNewObject) {172newObj[prop] = { _$type: 'object', _$skipped: 'SKIPPED STYLE' };173continue;174}175}176if (hierarchyNav.has(path)) {177newObj[prop] = hierarchyNav.get(path);178continue;179}180181if (doNotAccess.includes(path)) {182continue;183}184try {185const isOwnProp =186obj.hasOwnProperty && obj.hasOwnProperty(key) && !inheritedProps.includes(key);187const value = await extractPropValue(obj, key, path, !isOwnProp);188if (value && typeof value === 'string' && value.startsWith('REF:') && !isOwnProp) {189// don't assign here190//console.log('skipping ref', value);191} else {192newObj[prop] = value;193}194} catch (err) {195newObj[prop] = err.toString();196}197}198if (obj.prototype) {199let instance;200let constructorException;201try {202instance = await new obj();203} catch (err) {204constructorException = err.toString();205}206if (constructorException) {207newObj['new()'] = { _$type: 'constructor', _$constructorException: constructorException };208} else {209try {210newObj['new()'] = await extractPropsFromObject(instance, parentPath + '.new()');211newObj['new()']._$type = 'constructor';212} catch (err) {213newObj['new()'] = err.toString();214}215}216}217return newObj;218}219220async function loadProtoHierarchy(obj, parentPath) {221const hierarchy = [];222let proto = obj;223if (typeof proto === 'function') return hierarchy;224225while (!!proto) {226proto = Object.getPrototypeOf(proto);227228if (!proto) break;229230try {231let name = getObjectName(proto);232if (name && !hierarchy.includes(name)) hierarchy.push(name);233234if (loadedObjects.has(proto)) continue;235236let path = `${selfName}.${name}`;237let topType = name.split('.').shift();238if (!(topType in self)) {239path = 'detached.' + name;240}241242if (!hierarchyNav.has(path)) {243hierarchyNav.set(path, {});244const extracted = await extractPropsFromObject(proto, path);245hierarchyNav.set(path, extracted);246if (!path.includes(`${selfName}.`)) {247detached[name] = extracted;248}249}250} catch (err) {}251}252return hierarchy;253}254255async function extractPropValue(obj, key, path, isInherited) {256if (obj === null || obj === undefined || !key) {257return undefined;258}259260let accessException;261let value = await new Promise(async (resolve, reject) => {262let didResolve = false;263// if you wait on a promise, it will hang!264const t = setTimeout(() => reject('Likely a Promise'), 600);265try {266const p = await obj[key];267if (didResolve) return;268didResolve = true;269clearTimeout(t);270resolve(p);271} catch (err) {272if (didResolve) return;273clearTimeout(t);274reject(err);275}276}).catch(err => {277accessException = err;278});279280if (281value &&282path !== `${selfName}.document` &&283(typeof value === 'function' || typeof value === 'object' || typeof value === 'symbol')284) {285if (loadedObjects.has(value)) {286// TODO: re-enable invoking re-used functions once we are on stable ground with chrome flags287const shouldContinue = false; //typeof value === 'function' && (isInherited || !path.replace(String(key), '').includes(String(key)));288if (!shouldContinue) return 'REF: ' + loadedObjects.get(value);289}290// safari will end up in an infinite loop since each plugin is a new object as your traverse291if (path.includes('.navigator') && path.endsWith('.enabledPlugin')) {292return `REF: ${selfName}.navigator.plugins.X`;293}294loadedObjects.set(value, path);295}296297let details = {};298if (value && (typeof value === 'object' || typeof value === 'function')) {299details = await extractPropsFromObject(value, path);300}301const descriptor = await getDescriptor(obj, key, accessException, path);302303if (!Object.keys(descriptor).length && !Object.keys(details).length) return undefined;304const prop = Object.assign(details, descriptor);305if (prop._$value === 'REF: ' + path) {306prop._$value = undefined;307}308309return prop;310}311312async function getDescriptor(obj, key, accessException, path) {313const objDesc = Object.getOwnPropertyDescriptor(obj, key);314315if (objDesc) {316let value;317try {318value = objDesc.value;319if (!value && !accessException) {320value = obj[key];321}322} catch (err) {}323324let type = typeof value;325value = getJsonUsableValue(value, key);326const functionDetails = await getFunctionDetails(value, obj, key, type, path);327type = functionDetails.type;328329const flags = [];330if (objDesc.configurable) flags.push('c');331if (objDesc.enumerable) flags.push('e');332if (objDesc.writable) flags.push('w');333334return {335_$type: type,336_$function: functionDetails.func,337_$invocation: functionDetails.invocation,338_$flags: flags.join(''),339_$accessException: accessException ? accessException.toString() : undefined,340_$value: value,341_$get: objDesc.get ? objDesc.get.toString() : undefined,342_$set: objDesc.set ? objDesc.set.toString() : undefined,343_$getToStringToString: objDesc.get ? objDesc.get.toString.toString() : undefined,344_$setToStringToString: objDesc.set ? objDesc.set.toString.toString() : undefined,345};346} else {347const plainObject = {};348349if (accessException && String(accessException).includes('Likely a Promise')) {350plainObject._$value = 'Likely a Promise';351} else if (accessException) return plainObject;352let value;353try {354value = obj[key];355} catch (err) {}356357let type = typeof value;358if (value && Array.isArray(value)) type = 'array';359360const functionDetails = await getFunctionDetails(value, obj, key, type, path);361plainObject._$type = functionDetails.type;362plainObject._$value = getJsonUsableValue(value, key);363plainObject._$function = functionDetails.func;364plainObject._$invocation = functionDetails.invocation;365366return plainObject;367}368}369370async function getFunctionDetails(value, obj, key, type, path) {371let func;372let invocation;373if (type === 'undefined') type = undefined;374if (type === 'function') {375try {376func = String(value);377} catch (err) {378func = err.toString();379}380try {381if (!doNotInvoke.includes(key) && !doNotInvoke.includes(path) && !value.prototype) {382invocation = await new Promise(async (resolve, reject) => {383const c = setTimeout(() => reject('Promise-like'), 650);384let didReply = false;385try {386let answer = obj[key]();387if (answer && answer.on) {388answer.on('error', err => {389console.log('Error', err, obj, key);390});391}392answer = await answer;393394if (didReply) return;395clearTimeout(c);396didReply = true;397resolve(answer);398} catch (err) {399if (didReply) return;400didReply = true;401clearTimeout(c);402reject(err);403}404});405}406} catch (err) {407invocation = err ? err.toString() : err;408}409}410411return {412type,413func,414invocation: func || invocation !== undefined ? getJsonUsableValue(invocation) : undefined,415};416}417418function getJsonUsableValue(value, key) {419if (key && skipValues.includes(key)) {420return 'SKIPPED VALUE';421}422423try {424if (value && typeof value === 'symbol') {425value = '' + String(value);426} else if (value && (value instanceof Promise || typeof value.then === 'function')) {427value = 'Promise';428} else if (value && typeof value === 'object') {429const values = [];430431if (loadedObjects.has(value)) {432return 'REF: ' + loadedObjects.get(value);433}434435if (value.join !== undefined) {436// is array437for (const prop in value) {438values.push(getJsonUsableValue(value[prop]));439}440return `[${values.join(',')}]`;441}442443for (const prop in value) {444if (value.hasOwnProperty(prop)) {445values.push(prop + ': ' + getJsonUsableValue(value[prop]));446}447}448return `{${values.map(x => x.toString()).join(',')}}`;449} else if (typeof value === 'function') {450return value.toString();451} else if (value && typeof value === 'string') {452if (pageUrl) {453while (value.includes(pageUrl)) {454value = value.replace(pageUrl, '<URL>');455}456}457if (pageHost) {458while (value.includes(pageHost)) {459value = value.replace(pageHost, '<HOST>');460}461}462463value = value.replace(/<url>\:\d+\:\d+/g, '<url>:<lines>');464} else {465return value;466}467} catch (err) {468value = err.toString();469}470return value;471}472473function getObjectName(obj) {474if (obj === Object) return 'Object';475if (obj === Object.prototype) return 'Object.prototype';476try {477if (typeof obj === 'symbol') {478return '' + String(obj);479}480} catch (err) {}481try {482let name = obj[Symbol.toStringTag];483if (!name) {484try {485name = obj.name;486} catch (err) {}487}488489if (obj.constructor) {490const constructorName = obj.constructor.name;491492if (493constructorName &&494constructorName !== Function.name &&495constructorName !== Object.name496) {497name = constructorName;498}499}500501if ('prototype' in obj) {502name = obj.prototype[Symbol.toStringTag] || obj.prototype.name || name;503if (name) return name;504}505506if (typeof obj === 'function') {507if (name && name !== Function.name) return name;508return obj.constructor.name;509}510511if (!name) return;512513return name + '.prototype';514} catch (err) {}515}516517async function runAndSave() {518self.addEventListener('unhandledrejection', function (promiseRejectionEvent) {519console.log(promiseRejectionEvent);520});521522const props = await extractPropsFromObject(self, selfName);523524await fetch(saveToUrl, {525method: 'POST',526body: JSON.stringify({527[selfName]: props,528detached,529}),530headers: {531'Content-Type': 'application/json',532'Page-Name': pageName,533},534});535}536537async function run(obj, parentPath, extractKeys = []) {538const result = await extractPropsFromObject(obj, parentPath);539540if (extractKeys && extractKeys.length) {541const extracted = {};542for (const key of extractKeys) {543extracted[key] = result[key];544}545return JSON.stringify({ window: extracted, windowKeys: Object.keys(result) });546}547// NOTE: need to stringify to make sure this transfers same as it will from a browser window548return JSON.stringify({ window: result, detached });549}550551this.run = run;552this.runAndSave = runAndSave;553554return this;555}556557module.exports = DomExtractor;558if (typeof exports !== 'undefined') exports.default = DomExtractor;559560561