1
0
mirror of https://github.com/webrecorder/pywb.git synced 2025-03-15 00:03:28 +01:00

server side rewriting: (#475)

- fixed edge case in jsonP rewriting where no callback name is supplied only ? but body has normal jsonP callback (url = https://geolocation.onetrust.com/cookieconsentpub/v1/geo/countries/EU?callback=?)
  - made the `!self.__WB_pmw` server side inject match the client side one done via wombat
  - added regex's for eval override to JSWombatProxyRules

wombat:
  - added eval override in order to ensure AD network JS does not lockup the browser (cnn.com)
  - added returning a proxied value for window.parent from the window proxy object in order to ensure AD network JS does not lock up browser (cnn.com, boston.com, et. al.)
  - ensured that the read only storageArea property of 'StorageEvents' fired by the custom storage class is present (spec)
  - ensured that userland cannot create new instances of Storage and that localStorage instanceof Storage works with our override (spec)
  - ensured that assignments to SomeElement.[innerHTML|outerHTML], iframe.srcdoc, or style.textContent are more correctly handled in particular doing script.[innerHTML|outerHTML] = <JS>
  - ensured that the test used in the isArgumentsObj function does not throw errors IFF the pages JS has made it so when toString is called (linkedin.com)
  - ensured that Web Worker construction when using the optional options object, the options get supplied to the create worker (spec)
  - ensured that the expected TypeError exception thrown from DomConstructors of overridden interfaces are thrown when their pre-conditions are not met (spec)
  - ensured that wombat worker rewriting skipes data urls which should not be rewritten
  - ensured the bound function returned by the window function is the same on subsequent retrievals fixes #474

  tests:
   - added direct testing of wombat's parts
This commit is contained in:
John Berlin 2019-06-20 22:06:41 -04:00 committed by Ilya Kreymer
parent fb0a927238
commit 6437dab1f4
23 changed files with 2295 additions and 1228 deletions

View File

@ -47,7 +47,7 @@ jobs:
- stage: test
name: "Wombat Tests"
language: node_js
node_js: 12.0.0
node_js: 12.4.0
env:
- WR_TEST=no WOMBAT_TEST=yes
addons:

View File

@ -19,6 +19,12 @@ class JSONPRewriter(StreamingRewriter):
m_callback = self.CALLBACK.search(self.url_rewriter.wburl.url)
if not m_callback:
return string
if m_callback.group(1) == '?':
# this is a very sharp edge case e.g. callback=?
# since we only have this string[m_json.end(1):]
# would cut off the name of the CB if any is included
# so we just pass the string through
return string
string = m_callback.group(1) + string[m_json.end(1):]
return string

View File

@ -65,7 +65,7 @@ class JSWombatProxyRules(RxRules):
local_init_func = '\nvar {0} = function(name) {{\
return (self._wb_wombat && self._wb_wombat.local_init && \
self._wb_wombat.local_init(name)) || self[name]; }};\n\
if (!self.__WB_pmw) {{ self.__WB_pmw = function(obj) {{ return obj; }} }}\n\
if (!self.__WB_pmw) {{ self.__WB_pmw = function(obj) {{ this.__WB_source = obj; return this; }} }}\n\
{{\n'
local_check_this_fn = 'var {0} = function (thisObj) {{ \
if (thisObj && thisObj._WB_wombat_obj_proxy) return thisObj._WB_wombat_obj_proxy; return thisObj; }};'
@ -102,6 +102,8 @@ if (thisObj && thisObj._WB_wombat_obj_proxy) return thisObj._WB_wombat_obj_proxy
prop_str = '|'.join(self.local_objs)
rules = [
(r'\beval\s*\(', self.add_prefix('WB_wombat_runEval(function _____evalIsEvil(_______eval_arg$$) { return eval(_______eval_arg$$); }.bind(this)).'), 0),
(r'\beval\b', self.add_prefix('WB_wombat_'), 0),
(r'(?<=\.)postMessage\b\(', self.add_prefix('__WB_pmw(self).'), 0),
(r'(?<![$.])\s*location\b\s*[=]\s*(?![=])', self.add_suffix(check_loc), 0),
(r'\breturn\s+this\b\s*(?![.$])', self.replace_str(this_rw), 0),

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,63 +1,70 @@
import get from 'lodash-es/get';
/**
* @type {TestOverwatch}
* @typedef {Object} TestOverwatchInit
* @property {HTMLIFrameElement} sandbox
* @property {Object} [domStructure]
* @property {boolean} [direct]
*/
window.TestOverwatch = class TestOverwatch {
/**
* @param {Object} domStructure
* @param {TestOverwatchInit} init
*/
constructor(domStructure) {
constructor({ sandbox, domStructure, direct }) {
/**
* @type {{document: Document, window: Window}}
*/
this.ownContextWinDoc = { window, document };
this.wbMessages = { load: false };
this.domStructure = domStructure;
this.direct = direct;
/**
* @type {HTMLIFrameElement}
*/
this.sandbox = domStructure.sandbox;
window.addEventListener(
'message',
event => {
if (event.data) {
const { data } = event;
switch (data.wb_type) {
case 'load':
this.wbMessages.load =
this.wbMessages.load || data.readyState === 'complete';
this.domStructure.load.url.data = data.url;
this.domStructure.load.title.data = data.title;
this.domStructure.load.readyState.data = data.readyState;
break;
case 'replace-url':
this.wbMessages['replace-url'] = data;
this.domStructure.replaceURL.url.data = data.url;
this.domStructure.replaceURL.title.data = data.title;
break;
case 'title':
this.wbMessages.title = data.title;
this.domStructure.titleMsg.data = data.title;
break;
case 'hashchange':
this.domStructure.hashchange.data = data.title;
this.wbMessages.hashchange = data.hash;
break;
case 'cookie':
this.domStructure.cookie.domain = data.domain;
this.domStructure.cookie.cookie = data.cookie;
this.wbMessages.cookie = data;
break;
default:
this.domStructure.unknown.data = JSON.stringify(data);
break;
this.sandbox = sandbox;
if (!this.direct) {
window.addEventListener(
'message',
event => {
if (event.data) {
const { data } = event;
switch (data.wb_type) {
case 'load':
this.wbMessages.load =
this.wbMessages.load || data.readyState === 'complete';
this.domStructure.load.url.data = data.url;
this.domStructure.load.title.data = data.title;
this.domStructure.load.readyState.data = data.readyState;
break;
case 'replace-url':
this.wbMessages['replace-url'] = data;
this.domStructure.replaceURL.url.data = data.url;
this.domStructure.replaceURL.title.data = data.title;
break;
case 'title':
this.wbMessages.title = data.title;
this.domStructure.titleMsg.data = data.title;
break;
case 'hashchange':
this.domStructure.hashchange.data = data.title;
this.wbMessages.hashchange = data.hash;
break;
case 'cookie':
this.domStructure.cookie.domain = data.domain;
this.domStructure.cookie.cookie = data.cookie;
this.wbMessages.cookie = data;
break;
default:
this.domStructure.unknown.data = JSON.stringify(data);
break;
}
}
}
},
false
);
},
false
);
}
}
/**
@ -67,7 +74,9 @@ window.TestOverwatch = class TestOverwatch {
* environment purity.
*/
initSandbox() {
this.domStructure.reset();
if (this.domStructure) {
this.domStructure.reset();
}
this.wbMessages = { load: false };
this.sandbox.contentWindow._WBWombatInit(this.sandbox.contentWindow.wbinfo);
this.sandbox.contentWindow.WombatTestUtil = {

View File

@ -4,25 +4,25 @@
"main": "index.js",
"license": "GPL-3.0",
"devDependencies": {
"@types/fs-extra": "^5.0.5",
"ava": "^1.4.1",
"chokidar": "^2.1.5",
"chrome-remote-interface-extra": "^1.0.0",
"@types/fs-extra": "^7.0.0",
"ava": "^2.1.0",
"chokidar": "^3.0.1",
"chrome-remote-interface-extra": "^1.1.1",
"eslint": "^5.16.0",
"eslint-config-prettier": "^4.2.0",
"eslint-plugin-prettier": "^3.0.1",
"fastify": "^2.3.0",
"eslint-config-prettier": "^5.0.0",
"eslint-plugin-prettier": "^3.1.0",
"fastify": "^2.5.0",
"fastify-favicon": "^2.0.0",
"fastify-graceful-shutdown": "^2.0.1",
"fastify-static": "^2.4.0",
"fs-extra": "^7.0.1",
"fastify-static": "^2.5.0",
"fs-extra": "^8.0.1",
"lodash-es": "^4.17.11",
"prettier": "^1.17.0",
"rollup": "^1.10.1",
"prettier": "^1.18.2",
"rollup": "^1.15.6",
"rollup-plugin-babel-minify": "^8.0.0",
"rollup-plugin-cleanup": "^3.1.1",
"rollup-plugin-commonjs": "^9.3.4",
"rollup-plugin-node-resolve": "^4.2.3",
"rollup-plugin-commonjs": "^10.0.0",
"rollup-plugin-node-resolve": "^5.0.3",
"rollup-plugin-uglify": "^6.0.2",
"rollup-plugin-uglify-es": "^0.0.1",
"tls-keygen": "^3.7.0"
@ -32,8 +32,8 @@
"build-dev": "ALL=1 rollup -c rollup.config.dev.js",
"build-dev-watch": "rollup --watch -c rollup.config.dev.js",
"build-dev-watch-proxy": "PROXY=1 rollup --watch -c rollup.config.dev.js",
"build-test": "rollup -c rollup.config.test.js && rollup -c ./internal/rollup.testPageBundle.config.js",
"build-full-test": "rollup -c rollup.config.test.js",
"build-test": "rollup -c rollup.config.test.js",
"build-full-test": "rollup -c rollup.config.test.js && rollup -c ./internal/rollup.testPageBundle.config.js",
"build-test-bundle": "rollup -c ./internal/rollup.testPageBundle.config.js",
"test": "ava --verbose"
},
@ -50,16 +50,17 @@
"!test/assests/*",
"!test/helpers/extractOrigFunky.js"
],
"helpers": [
"test/helpers/**.js"
],
"sources": [
"!src/**/*"
"src/**/*"
]
},
"resolutions": {
"*/**/graceful-fs": "~4.1.15",
"*/**/jsonfile": "~5.0.0",
"*/**/universalify": "~0.1.2"
"*/**/graceful-fs": "~4.1.15"
},
"dependencies": {
"chrome-launcher": "^0.10.5"
"just-launch-chrome": "^1.0.0"
}
}

View File

@ -38,5 +38,26 @@ export default [
exports: 'none'
},
plugins: [noStrict]
},
{
input: 'src/wbWombat.js',
output: {
name: 'wombat',
file: path.join(baseTestOutput, 'wombatDirect.js'),
sourcemap: false,
format: 'es'
},
plugins: [
noStrict,
{
renderChunk(code) {
return code.replace(
/(this\._wb_wombat\.actual\s=\strue;)/gi,
`this._wb_wombat.actual = true;
this.wombat = wombat;`
);
}
}
]
}
];

View File

@ -1,4 +1,8 @@
import { addToStringTagToClass, ensureNumber } from './wombatUtils';
import {
addToStringTagToClass,
ensureNumber,
ThrowExceptions
} from './wombatUtils';
/**
* A re-implementation of the Storage interface.
@ -13,6 +17,12 @@ import { addToStringTagToClass, ensureNumber } from './wombatUtils';
* @see https://html.spec.whatwg.org/multipage/webstorage.html#the-storage-interface
*/
export default function Storage(wombat, proxying) {
if (ThrowExceptions.yes) {
// there is no constructor exposed for this interface however there is an
// interface object exposed, thus we must throw an TypeError if userland
// attempts to create us
throw new TypeError('Illegal constructor');
}
// hide our values from enumeration, spreed, et al
Object.defineProperties(this, {
data: {
@ -111,9 +121,14 @@ Storage.prototype.fireEvent = function fireEvent(key, oldValue, newValue) {
oldValue: oldValue,
url: this.wombat.$wbwindow.WB_wombat_location.href
});
// storage is a read only property of StorageEvent
// that must be on the fired instance of the event
Object.defineProperty(sevent, 'storageArea', {
value: this,
writable: false,
configurable: false
});
sevent._storageArea = this;
this.wombat.storage_listeners.map(sevent);
};

View File

@ -3,8 +3,12 @@ import FuncMap from './funcMap';
import Storage from './customStorage';
import WombatLocation from './wombatLocation';
import AutoFetchWorker from './autoFetchWorker';
import { wrapSameOriginEventListener, wrapEventListener } from './listeners';
import { addToStringTagToClass, autobind } from './wombatUtils';
import { wrapEventListener, wrapSameOriginEventListener } from './listeners';
import {
addToStringTagToClass,
autobind,
ThrowExceptions
} from './wombatUtils';
/**
* @param {Window} $wbwindow
@ -276,6 +280,11 @@ function Wombat($wbwindow, wbinfo) {
},
addEventListener: eTargetProto.addEventListener,
removeEventListener: eTargetProto.removeEventListener,
// some sites do funky things with the toString function
// (e.g. if used throw error or deny operation) hence we
// need a surefire and safe way to tell us what an object
// or function is hence Objects native toString
objToString: Object.prototype.toString,
wbSheetMediaQChecker: null,
XHRopen: null
};
@ -410,10 +419,19 @@ Wombat.prototype.getPageUnderModifier = function() {
* @return {boolean}
*/
Wombat.prototype.isNativeFunction = function(funToTest) {
if (!funToTest) return false;
if (!funToTest || typeof funToTest !== 'function') return false;
return this.wb_funToString.call(funToTest).indexOf('[native code]') >= 0;
};
/**
* Returns T/F indicating if the supplied argument is a string or not
* @param {*} arg
* @return {boolean}
*/
Wombat.prototype.isString = function(arg) {
return arg != null && Object.getPrototypeOf(arg) === String.prototype;
};
/**
* Returns T/F indicating if the supplied element may have attributes that
* are auto-fetched
@ -491,7 +509,13 @@ Wombat.prototype.isArgumentsObj = function(maybeArgumentsObj) {
) {
return false;
}
return maybeArgumentsObj.toString() === '[object Arguments]';
try {
return (
this.utilFns.objToString.call(maybeArgumentsObj) === '[object Arguments]'
);
} catch (e) {
return false;
}
};
/**
@ -508,7 +532,7 @@ Wombat.prototype.deproxyArrayHandlingArgumentsObj = function(
if (
!maybeArgumentsObj ||
maybeArgumentsObj instanceof NodeList ||
maybeArgumentsObj.length == 0
!maybeArgumentsObj.length
) {
return maybeArgumentsObj;
}
@ -592,6 +616,53 @@ Wombat.prototype.shouldRewriteAttr = function(tagName, attr) {
);
};
/**
* Returns T/F indicating if the script tag being rewritten should not
* have its text contents wrapped based on the supplied script type.
* @param {?string} scriptType
* @return {boolean}
*/
Wombat.prototype.skipWrapScriptBasedOnType = function(scriptType) {
if (!scriptType) return false;
if (scriptType.indexOf('json') >= 0) return true;
return scriptType.indexOf('text/template') >= 0;
};
/**
* Returns T/F indicating if the script tag being rewritten should not
* have its text contents wrapped based on heuristic analysis of its
* text contents.
* @param {?string} text
* @return {boolean}
*/
Wombat.prototype.skipWrapScriptTextBasedOnText = function(text) {
if (
!text ||
text.indexOf('_____WB$wombat$assign$function_____') >= 0 ||
text.indexOf('<') === 0
) {
return true;
}
var override_props = [
'window',
'self',
'document',
'location',
'top',
'parent',
'frames',
'opener'
];
for (var i = 0; i < override_props.length; i++) {
if (text.indexOf(override_props[i]) >= 0) {
return false;
}
}
return true;
};
/**
* Returns T/F indicating if the supplied DOM Node has child Elements/Nodes.
* Note this function should be used when the Node(s) being considered can
@ -699,7 +770,10 @@ Wombat.prototype.wrapScriptTextJsProxy = function(scriptText) {
return (
'var _____WB$wombat$assign$function_____ = function(name) {return (self._wb_wombat && ' +
'self._wb_wombat.local_init &&self._wb_wombat.local_init(name)) || self[name]; };\n' +
'if (!self.__WB_pmw) { self.__WB_pmw = function(obj) { return obj; } }\n{\n' +
'var _____WB$wombat$check$this$function_____ = function(thisObj) {' +
'if (thisObj && thisObj._WB_wombat_obj_proxy) return thisObj._WB_wombat_obj_proxy;' +
'return thisObj; }\nif (!self.__WB_pmw) { self.__WB_pmw = function(obj) { ' +
'this.__WB_source = obj; return this; } }\n{\n' +
'let window = _____WB$wombat$assign$function_____("window");\n' +
'let self = _____WB$wombat$assign$function_____("self");\n' +
'let document = _____WB$wombat$assign$function_____("document");\n' +
@ -708,7 +782,7 @@ Wombat.prototype.wrapScriptTextJsProxy = function(scriptText) {
'let parent = _____WB$wombat$assign$function_____("parent");\n' +
'let frames = _____WB$wombat$assign$function_____("frames");\n' +
'let opener = _____WB$wombat$assign$function_____("opener");\n' +
scriptText +
scriptText.replace(this.DotPostMessageRe, '.__WB_pmw(self.window)$1') +
'\n\n}'
);
};
@ -1194,16 +1268,20 @@ Wombat.prototype.objToProxy = function(obj) {
* @param {*} obj
* @param {*} prop
* @param {Array<string>} ownProps
* @param {Object} fnCache
* @return {*}
*/
Wombat.prototype.defaultProxyGet = function(obj, prop, ownProps) {
Wombat.prototype.defaultProxyGet = function(obj, prop, ownProps, fnCache) {
switch (prop) {
case '__WBProxyRealObj__':
return obj;
case 'location':
case 'WB_wombat_location':
return obj.WB_wombat_location;
case '_WB_wombat_obj_proxy':
return obj._WB_wombat_obj_proxy;
case '__WB_pmw':
return obj[prop];
case 'constructor':
// allow tests such as self.constructor === Window to work
// you can't create a new instance of window using its constructor
@ -1228,7 +1306,17 @@ Wombat.prototype.defaultProxyGet = function(obj, prop, ownProps) {
break;
}
}
return retVal.bind(obj);
// due to specific use cases involving native functions
// we must return the
var cachedFN = fnCache[prop];
if (!cachedFN || cachedFN.original !== retVal) {
cachedFN = {
original: retVal,
boundFn: retVal.bind(obj)
};
fnCache[prop] = cachedFN;
}
return cachedFN.boundFn;
} else if (type === 'object' && retVal && retVal._WB_wombat_obj_proxy) {
if (retVal instanceof Window) {
this.initNewWindowWombat(retVal);
@ -1390,9 +1478,10 @@ Wombat.prototype.styleReplacer = function(match, n1, n2, n3, offset, string) {
* injected functions are present in the supplied window.
*
* They could be absent due to how certain pages use iframes
* @param {Window} win
* @param {?Window} win
*/
Wombat.prototype.ensureServerSideInjectsExistOnWindow = function(win) {
if (!win) return;
if (typeof win._____WB$wombat$check$this$function_____ !== 'function') {
win._____WB$wombat$check$this$function_____ = function(thisObj) {
if (thisObj && thisObj._WB_wombat_obj_proxy)
@ -1412,6 +1501,51 @@ Wombat.prototype.ensureServerSideInjectsExistOnWindow = function(win) {
}
};
/**
* Due to the fact that we override specific DOM constructors, e.g. Worker,
* the normal TypeErrors are not thrown if the pre-conditions for those
* constructors are not met.
*
* Code that performs polyfills or browser feature detection based
* on those TypeErrors will not work as expected if we do not perform
* those checks ourselves (Note we use Chrome's error messages)
*
* This function checks for those pre-conditions and throws an TypeError
* with the appropriate message if a pre-condition is not met
* - `this` instanceof Window is false (requires new)
* - supplied required arguments
*
* @param {Object} thisObj
* @param {string} what
* @param {Object} [args]
* @param {number} [numRequiredArgs]
*/
Wombat.prototype.domConstructorErrorChecker = function(
thisObj,
what,
args,
numRequiredArgs
) {
var needArgs = typeof numRequiredArgs === 'number' ? numRequiredArgs : 1;
var erorMsg;
if (thisObj instanceof Window) {
erorMsg =
"Failed to construct '" +
what +
"': Please use the 'new' operator, this DOM object constructor cannot be called as a function.";
} else if (args && args.length < needArgs) {
erorMsg =
"Failed to construct '" +
what +
"': " +
needArgs +
' argument required, but only 0 present.';
}
if (erorMsg) {
throw new TypeError(erorMsg);
}
};
/**
* Rewrites the arguments supplied to an function of the Node interface
* @param {Object} fnThis
@ -1431,8 +1565,9 @@ Wombat.prototype.rewriteNodeFuncArgs = function(
this.rewriteElemComplete(newNode);
break;
case Node.TEXT_NODE:
// newNode is the new child of fnThis (the parent node)
if (
newNode.tagName === 'STYLE' ||
fnThis.tagName === 'STYLE' ||
(newNode.parentNode && newNode.parentNode.tagName === 'STYLE')
) {
newNode.textContent = this.rewriteStyle(newNode.textContent);
@ -1912,51 +2047,10 @@ Wombat.prototype.rewriteScript = function(elem) {
if (elem.hasAttribute('src') || !elem.textContent || !this.$wbwindow.Proxy) {
return this.rewriteAttr(elem, 'src');
}
var type = elem.type;
if (
type &&
(type === 'application/json' || type.indexOf('text/template') !== -1)
) {
return false;
}
if (this.skipWrapScriptBasedOnType(elem.type)) return false;
var text = elem.textContent.trim();
if (
!text ||
text.indexOf('_____WB$wombat$assign$function_____') >= 0 ||
text.indexOf('<') === 0
) {
return false;
}
var override_props = [
'window',
'self',
'document',
'location',
'top',
'parent',
'frames',
'opener'
];
var contains_props = false;
for (var i = 0; i < override_props.length; i++) {
if (text.indexOf(override_props[i]) >= 0) {
contains_props = true;
break;
}
}
if (!contains_props) return false;
elem.textContent = this.wrapScriptTextJsProxy(
text.replace(this.DotPostMessageRe, '.__WB_pmw(self.window)$1')
);
if (this.skipWrapScriptTextBasedOnText(text)) return false;
elem.textContent = this.wrapScriptTextJsProxy(text);
return true;
};
@ -2230,7 +2324,6 @@ Wombat.prototype.rewriteHtmlFull = function(string, checkEndTag) {
if (changed) {
var new_html;
// if original had <html> tag, add full document HTML
if (string && string.indexOf('<html') >= 0) {
inner_doc.documentElement._no_rewrite = true;
@ -2529,9 +2622,7 @@ Wombat.prototype.rewriteSetTimeoutInterval = function(
argsObj
) {
// strings are primitives with a prototype or __proto__ of String depending on the browser
var rw =
argsObj[0] != null &&
Object.getPrototypeOf(argsObj[0]) === String.prototype;
var rw = this.isString(argsObj[0]);
// do not mess with the arguments object unless you want instant de-optimization
var args = rw ? new Array(argsObj.length) : argsObj;
if (rw) {
@ -2553,6 +2644,66 @@ Wombat.prototype.rewriteSetTimeoutInterval = function(
return originalFn.apply(thisObj, args);
};
/**
* Rewrites the value of used in to set SomeElement.[innerHTML|outerHTML]
* iframe.srcdoc, or style.textContent handling edge cases e.g. script tags.
*
* If the element is a style tag and it has a sheet after the new value is set
* it, the sheet, is checked for media rules.
*
* @param {Object} thisObj
* @param {Function} oSetter
* @param {?string} newValue
*/
Wombat.prototype.rewriteHTMLAssign = function(thisObj, oSetter, newValue) {
var res = newValue;
var tagName = thisObj.tagName;
if (!thisObj._no_rewrite) {
if (tagName === 'STYLE') {
res = this.rewriteStyle(newValue);
} else {
res = this.rewriteHtml(newValue);
if (tagName === 'SCRIPT' && res === newValue) {
// script tags are used to hold HTML for later use
// so we let the rewriteHtml function go first
// however in this case inner or outer html was
// used to populate a script tag with some JS
if (
!this.skipWrapScriptBasedOnType(thisObj.type) &&
!this.skipWrapScriptTextBasedOnText(newValue)
) {
res = this.wrapScriptTextJsProxy(res);
}
}
}
}
oSetter.call(thisObj, res);
if (
this.wbUseAFWorker &&
this.WBAutoFetchWorker &&
tagName === 'STYLE' &&
thisObj.sheet != null
) {
// got to preserve all the things
this.WBAutoFetchWorker.deferredSheetExtraction(thisObj.sheet);
}
};
/**
* Rewrites the value to be supplied to eval or our injected wrapper
* @param {Function} rawEvalOrWrapper
* @param {*} evalArg
* @return {*}
*/
Wombat.prototype.rewriteEvalArg = function(rawEvalOrWrapper, evalArg) {
var toBeEvald =
this.isString(evalArg) &&
evalArg.indexOf('_____WB$wombat$assign$function_____') === -1
? this.wrapScriptTextJsProxy(evalArg)
: evalArg;
return rawEvalOrWrapper(toBeEvald);
};
/**
* Applies an Event property getter override for the supplied property
* @param {string} attr
@ -2866,34 +3017,18 @@ Wombat.prototype.overrideHtmlAssign = function(elem, prop, rewriteGetter) {
if (!orig_setter) return;
var wombat = this;
var rewriteFn = this.rewriteHTMLAssign;
var setter = function overrideHTMLAssignSetter(orig) {
var res = orig;
if (!this._no_rewrite) {
// init_iframe_insert_obs(this);
if (this.tagName === 'STYLE') {
res = wombat.rewriteStyle(orig);
} else {
res = wombat.rewriteHtml(orig);
}
}
orig_setter.call(this, res);
if (
this.wbUseAFWorker &&
this.WBAutoFetchWorker &&
this.tagName === 'STYLE' &&
this.sheet != null
) {
// got preserve all the things
this.WBAutoFetchWorker.deferredSheetExtraction(this.sheet);
}
return rewriteFn(this, orig_setter, orig);
};
var wb_unrewrite_rx = this.wb_unrewrite_rx;
var getter = function overrideHTMLAssignGetter() {
var res = orig_getter.call(this);
if (!this._no_rewrite) {
return res.replace(wombat.wb_unrewrite_rx, '');
return res.replace(wb_unrewrite_rx, '');
}
return res;
};
@ -3164,7 +3299,7 @@ Wombat.prototype.overrideTextProtoGetSet = function(textProto, whichProp) {
* Overrides the constructor of an UIEvent object in order to ensure
* that the `view`, `relatedTarget`, and `target` arguments of the
* constructor are not a JS Proxy used by wombat.
* @param {Object} which
* @param {string} which
*/
Wombat.prototype.overrideAnUIEvent = function(which) {
var didOverrideKey = '__wb_' + which + '_overridden';
@ -3205,6 +3340,7 @@ Wombat.prototype.overrideAnUIEvent = function(which) {
}
this.$wbwindow[which] = (function(EventConstructor) {
return function NewEventConstructor(type, init) {
wombat.domConstructorErrorChecker(this, which, arguments);
if (init) {
if (init.view != null) {
init.view = wombat.proxyToObj(init.view);
@ -3234,7 +3370,9 @@ Wombat.prototype.overrideAnUIEvent = function(which) {
* @return {*}
*/
Wombat.prototype.rewriteParentNodeFn = function(fnThis, originalFn, argsObj) {
var argArr = this.rewriteElementsInArguments(argsObj);
var argArr = this._no_rewrite
? argsObj
: this.rewriteElementsInArguments(argsObj);
var thisObj = this.proxyToObj(fnThis);
if (originalFn.__WB_orig_apply) {
return originalFn.__WB_orig_apply(thisObj, argArr);
@ -3485,6 +3623,7 @@ Wombat.prototype.initCSSOMOverrides = function() {
var oCSSKV = this.$wbwindow.CSSKeywordValue;
this.$wbwindow.CSSKeywordValue = (function(CSSKeywordValue_) {
return function CSSKeywordValue(cssValue) {
wombat.domConstructorErrorChecker(this, 'CSSKeywordValue', arguments);
return new CSSKeywordValue_(wombat.rewriteStyle(cssValue));
};
})(this.$wbwindow.CSSKeywordValue);
@ -3521,6 +3660,7 @@ Wombat.prototype.initCSSOMOverrides = function() {
}
return originalSet.apply(this, newArgs);
};
var originalAppend = this.$wbwindow.StylePropertyMap.prototype.append;
this.$wbwindow.StylePropertyMap.prototype.append = function append() {
if (arguments.length <= 1) {
@ -3552,6 +3692,7 @@ Wombat.prototype.initAudioOverride = function() {
var wombat = this;
this.$wbwindow.Audio = (function(Audio_) {
return function Audio(url) {
wombat.domConstructorErrorChecker(this, 'Audio');
return new Audio_(wombat.rewriteUrl(url, true, 'oe_'));
};
})(this.$wbwindow.Audio);
@ -3688,6 +3829,7 @@ Wombat.prototype.initFontFaceOverride = function() {
var origFontFace = this.$wbwindow.FontFace;
this.$wbwindow.FontFace = (function(FontFace_) {
return function FontFace(family, source, descriptors) {
wombat.domConstructorErrorChecker(this, 'FontFace', arguments, 2);
var rwSource = source;
if (source != null) {
if (typeof source !== 'string') {
@ -3866,6 +4008,7 @@ Wombat.prototype.initHTTPOverrides = function() {
var orig_request = this.$wbwindow.Request;
this.$wbwindow.Request = (function(Request_) {
return function Request(input, init_opts) {
wombat.domConstructorErrorChecker(this, 'Request', arguments);
var newInitOpts = init_opts || {};
var newInput = input;
var inputType = typeof input;
@ -3894,6 +4037,9 @@ Wombat.prototype.initHTTPOverrides = function() {
})(this.$wbwindow.Request);
this.$wbwindow.Request.prototype = orig_request.prototype;
Object.defineProperty(this.$wbwindow.Request.prototype, 'constructor', {
value: this.$wbwindow.Request
});
}
if (this.$wbwindow.Response && this.$wbwindow.Response.prototype) {
@ -3912,6 +4058,7 @@ Wombat.prototype.initHTTPOverrides = function() {
var origEventSource = this.$wbwindow.EventSource;
this.$wbwindow.EventSource = (function(EventSource_) {
return function EventSource(url, configuration) {
wombat.domConstructorErrorChecker(this, 'EventSource', arguments);
var rwURL = url;
if (url != null) {
rwURL = wombat.rewriteUrl(url);
@ -3930,6 +4077,7 @@ Wombat.prototype.initHTTPOverrides = function() {
var origWebSocket = this.$wbwindow.WebSocket;
this.$wbwindow.WebSocket = (function(WebSocket_) {
return function WebSocket(url, configuration) {
wombat.domConstructorErrorChecker(this, 'WebSocket', arguments);
var rwURL = url;
if (url != null) {
rwURL = wombat.rewriteWSURL(url);
@ -4424,12 +4572,16 @@ Wombat.prototype.initWorkerOverrides = function() {
// Worker unrewrite postMessage
var orig_worker = this.$wbwindow.Worker;
this.$wbwindow.Worker = (function(Worker_) {
return function Worker(url) {
return new Worker_(wombat.rewriteWorker(url));
return function Worker(url, options) {
wombat.domConstructorErrorChecker(this, 'Worker', arguments);
return new Worker_(wombat.rewriteWorker(url), options);
};
})(orig_worker);
this.$wbwindow.Worker.prototype = orig_worker.prototype;
Object.defineProperty(this.$wbwindow.Worker.prototype, 'constructor', {
value: this.$wbwindow.Worker
});
this.$wbwindow.Worker._wb_worker_overriden = true;
}
@ -4440,12 +4592,20 @@ Wombat.prototype.initWorkerOverrides = function() {
// per https://html.spec.whatwg.org/multipage/workers.html#sharedworker
var oSharedWorker = this.$wbwindow.SharedWorker;
this.$wbwindow.SharedWorker = (function(SharedWorker_) {
return function SharedWorker(url) {
return new SharedWorker_(wombat.rewriteWorker(url));
return function SharedWorker(url, options) {
wombat.domConstructorErrorChecker(this, 'SharedWorker', arguments);
return new SharedWorker_(wombat.rewriteWorker(url), options);
};
})(oSharedWorker);
this.$wbwindow.SharedWorker.prototype = oSharedWorker.prototype;
Object.defineProperty(
this.$wbwindow.SharedWorker.prototype,
'constructor',
{
value: this.$wbwindow.SharedWorker
}
);
this.$wbwindow.SharedWorker.__wb_sharedWorker_overriden = true;
}
@ -4625,7 +4785,7 @@ Wombat.prototype.initHashChange = function() {
if (!message.wb_type) return;
if (message.wb_type === 'outer_hashchange') {
if (wombat.$wbwindow.location.hash !== message.hash) {
if (wombat.$wbwindow.location.hash != message.hash) {
wombat.$wbwindow.location.hash = message.hash;
}
}
@ -5008,6 +5168,11 @@ Wombat.prototype.initPresentationRequestOverride = function() {
var origPresentationRequest = this.$wbwindow.PresentationRequest;
this.$wbwindow.PresentationRequest = (function(PresentationRequest_) {
return function PresentationRequest(url) {
wombat.domConstructorErrorChecker(
this,
'PresentationRequest',
arguments
);
var rwURL = url;
if (url != null) {
if (Array.isArray(rwURL)) {
@ -5095,7 +5260,7 @@ Wombat.prototype.initStorageOverride = function() {
var session;
var pLocal = 'localStorage';
var pSession = 'sessionStorage';
ThrowExceptions.yes = false;
if (this.$wbwindow.Proxy) {
var storageProxyHandler = function() {
return {
@ -5133,6 +5298,9 @@ Wombat.prototype.initStorageOverride = function() {
this.defGetterProp(this.$wbwindow, pSession, function sessionStorage() {
return session;
});
// ensure localStorage instanceof Storage works
this.$wbwindow.Storage = Storage;
ThrowExceptions.yes = true;
};
/**
@ -5144,15 +5312,23 @@ Wombat.prototype.initWindowObjProxy = function($wbwindow) {
if (!$wbwindow.Proxy) return undefined;
var ownProps = this.getAllOwnProps($wbwindow);
var funCache = {};
var wombat = this;
var windowProxy = new $wbwindow.Proxy(
{},
{
get: function(target, prop) {
if (prop === 'top') {
return wombat.$wbwindow.WB_wombat_top._WB_wombat_obj_proxy;
switch (prop) {
case 'top':
return wombat.$wbwindow.WB_wombat_top._WB_wombat_obj_proxy;
case 'parent':
var realParent = $wbwindow.parent;
if (realParent === $wbwindow.WB_wombat_top) {
return $wbwindow.WB_wombat_top._WB_wombat_obj_proxy;
}
return realParent._WB_wombat_obj_proxy;
}
return wombat.defaultProxyGet($wbwindow, prop, ownProps);
return wombat.defaultProxyGet($wbwindow, prop, ownProps, funCache);
},
set: function(target, prop, value) {
switch (prop) {
@ -5212,6 +5388,7 @@ Wombat.prototype.initWindowObjProxy = function($wbwindow) {
if (propDescriptor.configurable === false) {
return false;
}
delete target[prop];
delete $wbwindow[prop];
return true;
},
@ -5239,11 +5416,12 @@ Wombat.prototype.initWindowObjProxy = function($wbwindow) {
Wombat.prototype.initDocumentObjProxy = function($document) {
this.initDocOverrides($document);
if (!this.$wbwindow.Proxy) return undefined;
var funCache = {};
var ownProps = this.getAllOwnProps($document);
var wombat = this;
var documentProxy = new this.$wbwindow.Proxy($document, {
get: function(target, prop) {
return wombat.defaultProxyGet($document, prop, ownProps);
return wombat.defaultProxyGet($document, prop, ownProps, funCache);
},
set: function(target, prop, value) {
if (prop === 'location') {
@ -5482,6 +5660,42 @@ Wombat.prototype.initWombatTop = function($wbwindow) {
this.defProp($wbwindow.Object.prototype, 'WB_wombat_top', setter, getter);
};
/**
* There is evil in this world because this is it.
* To quote the MDN / every sane person 'Do not ever use eval'.
* Why cause we gotta otherwise infinite loops.
*/
Wombat.prototype.initEvalOverride = function() {
var rewriteEvalArg = this.rewriteEvalArg;
var setNoop = function() {};
var wrappedEval = function wrappedEval(arg) {
return rewriteEvalArg(eval, arg);
};
this.defProp(
this.$wbwindow.Object.prototype,
'WB_wombat_eval',
setNoop,
function() {
return wrappedEval;
}
);
var runEval = function runEval(func) {
return {
eval: function(arg) {
return rewriteEvalArg(func, arg);
}
};
};
this.defProp(
this.$wbwindow.Object.prototype,
'WB_wombat_runEval',
setNoop,
function() {
return runEval;
}
);
};
/**
* Initialize wombat's internal state and apply all overrides
* @return {Object}
@ -5512,7 +5726,7 @@ Wombat.prototype.wombatInit = function() {
this.initDocWriteOpenCloseOverride();
// eval
// initEvalOverride();
this.initEvalOverride();
// Ajax, Fetch, Request, Response, EventSource, WebSocket
this.initHTTPOverrides();

View File

@ -54,3 +54,11 @@ export function autobind(clazz) {
}
}
}
/**
* Because we overriding specific interfaces (e.g. Storage) that do not expose
* an constructor only an interface object with our own we must have a way
* to indicate to our overrides when it is proper to throw exceptions
* @type {{yes: boolean}}
*/
export var ThrowExceptions = { yes: false };

View File

@ -24,6 +24,7 @@ WBWombat.prototype.noRewrite = function(url) {
!url ||
url.indexOf('blob:') === 0 ||
url.indexOf('javascript:') === 0 ||
url.indexOf('data:') === 0 ||
url.indexOf(this.info.prefix) === 0
);
};

View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Wombat sandbox direct</title>
<script>
window.wombatSandbox = {
window,
document,
originalLocation: location.href
};
window.wbinfo = {};
window.wbinfo.enable_auto_fetch = false;
window.wbinfo.top_url = location.href.replace('mp_', '');
window.wbinfo.url = 'https://tests.wombat.io/';
window.wbinfo.timestamp = '20180803160549';
window.wbinfo.request_ts = '20180803160549';
window.wbinfo.prefix = decodeURI(
`${window.location.protocol}//localhost:${window.location.port}/live/`
);
window.wbinfo.mod = 'mp_';
window.wbinfo.is_framed = true;
window.wbinfo.is_live = false;
window.wbinfo.coll = '';
window.wbinfo.proxy_magic = '';
window.wbinfo.static_prefix =
'https://content.webrecorder.io/static/bundle/';
window.wbinfo.wombat_ts = '20180803160549';
window.wbinfo.wombat_sec = '1533312349';
window.wbinfo.wombat_scheme = 'https';
window.wbinfo.wombat_host = 'tests.wombat.io';
window.wbinfo.wombat_opts = {};
window.wbinfo.state = '';
window.wbinfo.metadata = {};
</script>
</head>
<body>
<script src="/wombatDirect.js"></script>
</body>
</html>

View File

@ -126,7 +126,6 @@
<script>
const defaultText = ' not sent';
const domStructure = {
sandbox: document.getElementById('wombatSandbox'),
load: {
url: document.createTextNode(defaultText),
title: document.createTextNode(defaultText),
@ -187,7 +186,11 @@
document.getElementById('title-title').appendChild(domStructure.titleMsg);
document.getElementById('hash-hash').appendChild(domStructure.hashchange);
document.getElementById('unknown-msg').appendChild(domStructure.unknown);
window.overwatch = new TestOverwatch(domStructure);
window.overwatch = new TestOverwatch({
domStructure,
sandbox: document.getElementById('wombatSandbox'),
direct: false
});
</script>
</body>
</html>

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Wombat Direct Tests</title>
<script src="./testPageBundle.js"></script>
</head>
<body>
<iframe
id="wombatSandbox"
src="/live/20180803160549mp_/https://tests.direct.wombat.io/"
></iframe>
<script>
window.overwatch = new TestOverwatch({
sandbox: document.getElementById('wombatSandbox'),
direct: true
});
</script>
</body>
</html>

View File

@ -0,0 +1,225 @@
import test from 'ava';
import TestHelper from './helpers/testHelper';
/**
* @type {TestHelper}
*/
let helper = null;
test.before(async t => {
helper = await TestHelper.init(t, true);
});
test.beforeEach(async t => {
t.context.sandbox = helper.sandbox();
t.context.testPage = helper.testPage();
t.context.server = helper.server();
await t.context.sandbox.evaluate(() => {
window.fakeWombat = {
$wbwindow: {
WB_wombat_location: {
href: 'bogus url'
}
},
storage_listeners: {
sEvents: [],
map(sEvent) {
this.sEvents.push(sEvent);
}
}
};
});
});
test.after.always(async t => {
await helper.stop();
});
test('Storage - creation: should not throw errors', async t => {
const { sandbox, server } = t.context;
const creationPromise = sandbox.evaluate(() => {
new Storage();
});
await t.notThrowsAsync(creationPromise);
});
test('Storage - post creation: internal values should not be exposed', async t => {
const { sandbox, server } = t.context;
const testResult = await sandbox.evaluate(() => {
const storage = new Storage(window.fakeWombat, 'bogus value');
return { ...storage };
});
t.deepEqual(testResult, {});
});
test('Storage - getItem: the item set should be retrievable', async t => {
const { sandbox, server } = t.context;
const testResult = await sandbox.evaluate(() => {
const storage = new Storage(window.fakeWombat, 'bogus value');
const key = 'a';
const value = 'b';
storage.setItem(key, value);
return storage.getItem(key) === value;
});
t.true(testResult);
});
test('Storage - setItem: the item set should be mapped and an storage event fired', async t => {
const { sandbox, server } = t.context;
const testResult = await sandbox.evaluate(() => {
const storage = new Storage(window.fakeWombat, 'bogus value');
const key = 'a';
const value = 'b';
storage.setItem(key, value);
const events = window.fakeWombat.storage_listeners.sEvents;
const event = events[0];
return {
stored: storage.data[key] === value,
numEvents: events.length,
key: event.key === key,
newValue: event.newValue === value,
oldValue: event.oldValue === null,
storageArea: event.storageArea === storage,
url: event.url === 'bogus url'
};
});
t.deepEqual(testResult, {
stored: true,
numEvents: 1,
key: true,
newValue: true,
oldValue: true,
storageArea: true,
url: true
});
});
test('Storage - removeItem: the item set should be removable and an event should be fired indicating removal', async t => {
const { sandbox, server } = t.context;
const testResult = await sandbox.evaluate(() => {
const storage = new Storage(window.fakeWombat, 'bogus value');
const key = 'a';
const value = 'b';
storage.setItem(key, value);
storage.removeItem(key);
const events = window.fakeWombat.storage_listeners.sEvents;
const event = events[1];
return {
stored: storage.data[key] === undefined,
numEvents: events.length,
key: event.key === key,
newValue: event.newValue === null,
oldValue: event.oldValue === value,
storageArea: event.storageArea === storage,
url: event.url === 'bogus url'
};
});
t.deepEqual(testResult, {
stored: true,
numEvents: 2,
key: true,
newValue: true,
oldValue: true,
storageArea: true,
url: true
});
});
test('Storage - clear: should clear all stored items and an event should be fired indicating clearing', async t => {
const { sandbox, server } = t.context;
const testResult = await sandbox.evaluate(() => {
const storage = new Storage(window.fakeWombat, 'bogus value');
const key = 'a';
const value = 'b';
storage.setItem(key, value);
storage.clear();
const events = window.fakeWombat.storage_listeners.sEvents;
const event = events[1];
return {
numEvents: events.length,
key: event.key === null,
newValue: event.newValue === null,
oldValue: event.oldValue === null,
storageArea: event.storageArea === storage,
url: event.url === 'bogus url'
};
});
t.deepEqual(testResult, {
numEvents: 2,
key: true,
newValue: true,
oldValue: true,
storageArea: true,
url: true
});
});
test('Storage - key: should return the correct key given the keys index', async t => {
const { sandbox, server } = t.context;
const testResult = await sandbox.evaluate(() => {
const storage = new Storage(window.fakeWombat, 'bogus value');
const key1 = 'a1';
const key2 = 'a2';
const value1 = 'b1';
const value2 = 'b2';
storage.setItem(key1, value1);
storage.setItem(key2, value2);
return (
storage.key(0) === key1 &&
storage.key(1) === key2 &&
storage.key(2) === null
);
});
t.true(testResult);
});
test('Storage - fireEvent: should fire a StorageEvent with the supplied arguments', async t => {
const { sandbox, server } = t.context;
const testResult = await sandbox.evaluate(() => {
const storage = new Storage(window.fakeWombat, 'bogus value');
storage.fireEvent('a', 'b', 'c');
const events = window.fakeWombat.storage_listeners.sEvents;
const event = events[0];
return {
numEvents: events.length,
key: event.key === 'a',
newValue: event.newValue === 'c',
oldValue: event.oldValue === 'b',
storageArea: event.storageArea === storage,
url: event.url === 'bogus url'
};
});
t.deepEqual(testResult, {
numEvents: 1,
key: true,
newValue: true,
oldValue: true,
storageArea: true,
url: true
});
});
test('Storage - valueOf: should return the correct value', async t => {
const { sandbox, server } = t.context;
const testResult = await sandbox.evaluate(() => {
const storage = new Storage(window.fakeWombat, 'bogus value');
fakeWombat.$wbwindow['bogus value'] = storage;
return storage.valueOf() === storage;
});
t.true(testResult);
});
test('Storage - length: should return the correct value', async t => {
const { sandbox, server } = t.context;
const testResult = await sandbox.evaluate(() => {
const storage = new Storage(window.fakeWombat, 'bogus value');
const key1 = 'a1';
const key2 = 'a2';
const value1 = 'b1';
const value2 = 'b2';
storage.setItem(key1, value1);
storage.setItem(key2, value2);
return storage.length;
});
t.is(testResult, 2);
});

View File

@ -0,0 +1,235 @@
import test from 'ava';
import TestHelper from './helpers/testHelper';
/**
* @type {TestHelper}
*/
let helper = null;
test.before(async t => {
helper = await TestHelper.init(t, true);
});
test.beforeEach(async t => {
t.context.sandbox = helper.sandbox();
t.context.testPage = helper.testPage();
t.context.server = helper.server();
await t.context.sandbox.evaluate(() => {
window.fnsCalled = {
nTn: {
callCount: 0,
params: []
},
nTa: {
callCount: 0,
params: []
},
aTn: {
callCount: 0,
params: []
},
aTa: {
callCount: 0,
params: []
}
};
window.testedFNs = [
{
testName: 'nTn',
key() {
return 'key nTn';
},
value(param) {
window.fnsCalled.nTn.callCount += 1;
window.fnsCalled.nTn.params.push(param);
return 'value nTn';
}
},
{
testName: 'nTa',
key() {
return 'key nTa';
},
value: param => {
window.fnsCalled.nTa.callCount += 1;
window.fnsCalled.nTa.params.push(param);
return 'value nTa';
}
},
{
testName: 'aTn',
key: () => 'key aTn',
value(param) {
window.fnsCalled.aTn.callCount += 1;
window.fnsCalled.aTn.params.push(param);
return 'value aTn';
}
},
{
testName: 'aTa',
key: () => 'key aTa',
value: param => {
window.fnsCalled.aTa.callCount += 1;
window.fnsCalled.aTa.params.push(param);
return 'value aTa';
}
}
];
});
});
test.after.always(async t => {
await helper.stop();
});
test('Storage - creation: should not throw errors', async t => {
const { sandbox, server } = t.context;
const creationPromise = sandbox.evaluate(() => {
new Storage();
});
await t.notThrowsAsync(creationPromise);
});
test('FuncMap - set: normal and arrow functions should be added', async t => {
const { sandbox, server } = t.context;
const numKeys = await sandbox.evaluate(() => {
const fnm = new FuncMap();
for (let i = 0; i < testedFNs.length; i++) {
const { key, value } = testedFNs[i];
fnm.set(key, value);
}
return fnm._map.length;
});
t.is(numKeys, 4);
});
test('FuncMap - get: normal and arrow functions that are added should be retrieved per the key', async t => {
const { sandbox, server } = t.context;
const testResult = await sandbox.evaluate(() => {
const result = {
nTn: false,
nTa: false,
aTn: false,
aTa: false
};
const fnm = new FuncMap();
for (let i = 0; i < testedFNs.length; i++) {
const { key, value } = testedFNs[i];
fnm.set(key, value);
}
for (let i = 0; i < testedFNs.length; i++) {
const { testName, key, value } = testedFNs[i];
const mappedValue = fnm.get(key);
result[testName] = mappedValue === value && mappedValue() === value();
}
return result;
});
t.deepEqual(testResult, {
nTn: true,
nTa: true,
aTn: true,
aTa: true
});
});
test('FuncMap - find: should return the correct index of the internal mapping', async t => {
const { sandbox, server } = t.context;
const testResult = await sandbox.evaluate(() => {
const fnm = new FuncMap();
for (let i = 0; i < testedFNs.length; i++) {
const { key, value } = testedFNs[i];
fnm.set(key, value);
}
return testedFNs.every(({ key, value }) => {
const idx = fnm.find(key);
return fnm._map[idx][1] === value;
});
});
t.true(testResult);
});
test('FuncMap - add_or_get: should correctly add a function when no mapping exists and should return the existing mapping, not add a function, when a previous mapping was added', async t => {
const { sandbox, server } = t.context;
const testResult = await sandbox.evaluate(() => {
const fnm = new FuncMap();
let initerCalled = 0;
const initer = () => {
initerCalled += 1;
return () => {};
};
const key = () => {};
const fistCall = fnm.add_or_get(key, initer);
const secondCall = fnm.add_or_get(key, initer);
return {
initerCalled,
mappingCheck: fistCall === secondCall
};
});
await t.deepEqual(testResult, {
initerCalled: 1,
mappingCheck: true
});
});
test('FuncMap - remove: should remove the mapped function and return the mapped value if a mapping exists', async t => {
const { sandbox, server } = t.context;
const testResult = await sandbox.evaluate(() => {
const result = {
nTn: false,
nTa: false,
aTn: false,
aTa: false,
noMapping: false
};
const fnm = new FuncMap();
for (let i = 0; i < testedFNs.length; i++) {
const { key, value } = testedFNs[i];
fnm.set(key, value);
}
for (let i = 0; i < testedFNs.length; i++) {
const { testName, key, value } = testedFNs[i];
result[testName] = fnm.remove(key) === value;
}
result.noMapping = fnm.remove(() => {}) == null;
return result;
});
t.deepEqual(testResult, {
nTn: true,
nTa: true,
aTn: true,
aTa: true,
noMapping: true
});
});
test('FuncMap - map: should call every mapped with the supplied arguments', async t => {
const { sandbox, server } = t.context;
const testResult = await sandbox.evaluate(() => {
const fnm = new FuncMap();
for (let i = 0; i < testedFNs.length; i++) {
const { key, value } = testedFNs[i];
fnm.set(key, value);
}
fnm.map('the param 1');
fnm.map('the param 2');
return window.fnsCalled;
});
t.deepEqual(testResult, {
nTn: {
callCount: 2,
params: ['the param 1', 'the param 2']
},
nTa: {
callCount: 2,
params: ['the param 1', 'the param 2']
},
aTn: {
callCount: 2,
params: ['the param 1', 'the param 2']
},
aTa: {
callCount: 2,
params: ['the param 1', 'the param 2']
}
});
});

View File

@ -0,0 +1,161 @@
import test from 'ava';
import TestHelper from './helpers/testHelper';
import { NativeFnTest, SaveSrcSetDataSrcSet } from './helpers/testedValues';
/**
* @type {TestHelper}
*/
let helper = null;
test.before(async t => {
helper = await TestHelper.init(t, true);
});
test.beforeEach(async t => {
t.context.sandbox = helper.sandbox();
t.context.testPage = helper.testPage();
t.context.server = helper.server();
await helper.initWombat();
});
test.after.always(async t => {
await helper.stop();
});
test('getPageUnderModifier: should return the modifier the page is under', async t => {
const { sandbox } = t.context;
const testResult = await sandbox.evaluate(() =>
wombat.getPageUnderModifier()
);
t.is(testResult, 'mp_');
});
test('isNativeFunction: should return T/F indicating if a function is a native function or not', async t => {
const { sandbox } = t.context;
const testResult = await sandbox.evaluate(NativeFnTest.testFN);
t.deepEqual(testResult, NativeFnTest.expectedValue);
});
for (let i = 0; i < SaveSrcSetDataSrcSet.values.length; i++) {
const value = SaveSrcSetDataSrcSet.values[i];
test(`isSavedSrcSrcset: should return '${value.expected}' for '${
value.name
}'`, async t => {
const { sandbox } = t.context;
const testResult = await sandbox.evaluate(
SaveSrcSetDataSrcSet.testFnSS,
value.tagName,
value.parentElement
);
t.is(testResult, value.expected);
});
test(`isSavedDataSrcSrcset: should return '${value.expected}' for '${
value.name
}' if it has data.srcset and 'false' when it does not`, async t => {
const { sandbox } = t.context;
const testResult = await sandbox.evaluate(
SaveSrcSetDataSrcSet.testFnDSS,
value.tagName,
value.parentElement
);
t.deepEqual(testResult, {
with: value.expected,
without: false
});
});
}
test('isArgumentsObj: should return T/F indicating if the supplied object is the arguments object', async t => {
const { sandbox } = t.context;
const testResult = await sandbox.evaluate(() => ({
null: wombat.isArgumentsObj(null),
undefined: wombat.isArgumentsObj(undefined),
objToStringNotFn: wombat.isArgumentsObj({ toString: 1 }),
objToStringNotArgumentsObject: wombat.isArgumentsObj({}),
actualArgumentsObject: wombat.isArgumentsObj(
(function() {
return arguments;
})()
)
}));
t.deepEqual(testResult, {
null: false,
undefined: false,
objToStringNotFn: false,
objToStringNotArgumentsObject: false,
actualArgumentsObject: true
});
});
test('deproxyArrayHandlingArgumentsObj: should deproxy elements in both an array and the arguments object', async t => {
const { sandbox } = t.context;
const testResult = await sandbox.evaluate(() => {
const makeProxy = returnWhat =>
new Proxy(
{},
{
get(target, p, receiver) {
if (p === '__WBProxyRealObj__') return returnWhat;
return null;
}
}
);
const argumentsObjWithProxies = (function() {
return arguments;
})(makeProxy(1), makeProxy(2));
const argumentsDeproxied = wombat.deproxyArrayHandlingArgumentsObj(
argumentsObjWithProxies
);
const justAnArrayWithProxies = [makeProxy(3), makeProxy(4)];
const justArrayDeproxied = wombat.deproxyArrayHandlingArgumentsObj(
justAnArrayWithProxies
);
return {
argsDeproxied:
Array.isArray(argumentsDeproxied) &&
argumentsDeproxied !== argumentsObjWithProxies &&
argumentsDeproxied[0] === 1 &&
argumentsDeproxied[1] === 2,
arrayDeproxied:
Array.isArray(justArrayDeproxied) &&
justArrayDeproxied === justAnArrayWithProxies &&
justArrayDeproxied[0] === 3 &&
justArrayDeproxied[1] === 4
};
});
t.deepEqual(testResult, {
argsDeproxied: true,
arrayDeproxied: true
});
});
test('deproxyArrayHandlingArgumentsObj: should return the original argument if it is falsy, a NodeList, has not elements, or the length property is falsy', async t => {
const { sandbox } = t.context;
const testResult = await sandbox.evaluate(() => {
const nl = document.querySelectorAll('*');
const dpNL = wombat.deproxyArrayHandlingArgumentsObj(nl);
const zeroLenArray = [];
const zeroLenArguments = (function() {
return arguments;
})();
const falsyLength = {};
return {
falsey: wombat.deproxyArrayHandlingArgumentsObj(null) === null,
nodeList: dpNL instanceof NodeList && dpNL === nl,
zeroLenArray:
wombat.deproxyArrayHandlingArgumentsObj(zeroLenArray) === zeroLenArray,
zeroLenArguments:
wombat.deproxyArrayHandlingArgumentsObj(zeroLenArguments) ===
zeroLenArguments,
falsyLength:
wombat.deproxyArrayHandlingArgumentsObj(falsyLength) === falsyLength
};
});
t.deepEqual(testResult, {
falsey: true,
nodeList: true,
zeroLenArray: true,
zeroLenArguments: true,
falsyLength: true
});
});

View File

@ -1,24 +1,16 @@
const cp = require('child_process');
const path = require('path');
const os = require('os');
const fs = require('fs-extra');
const readline = require('readline');
const chromeFinder = require('chrome-launcher/dist/chrome-finder');
const criHelper = require('chrome-remote-interface-extra/lib/helper');
const { launch } = require('just-launch-chrome');
const Browser = require('chrome-remote-interface-extra/lib/browser/Browser');
const CHROME_PROFILE_PATH = path.join(os.tmpdir(), 'temp_chrome_profile-');
const winPos = !process.env.NO_MOVE_WINDOW ? '--window-position=2000,0' : '';
const chromeArgs = userDataDir => [
'--enable-automation',
const chromeArgs = [
'--force-color-profile=srgb',
'--remote-debugging-port=9222',
'--disable-background-networking',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--disable-backgrounding-occluded-windows',
'--disable-ipc-flooding-protection',
'--enable-features=NetworkService,NetworkServiceInProcess',
'--enable-features=NetworkService,NetworkServiceInProcess,AwaitOptimization',
'--disable-client-side-phishing-detection',
'--disable-default-apps',
'--disable-extensions',
@ -28,7 +20,7 @@ const chromeArgs = userDataDir => [
'--disable-sync',
'--disable-domain-reliability',
'--disable-infobars',
'--disable-features=site-per-process,TranslateUI',
'--disable-features=site-per-process,TranslateUI,BlinkGenPropertyTrees,LazyFrameLoading',
'--disable-breakpad',
'--disable-backing-store-limit',
'--metrics-recording-only',
@ -38,131 +30,26 @@ const chromeArgs = userDataDir => [
'--use-mock-keychain',
'--mute-audio',
'--autoplay-policy=no-user-gesture-required',
`--user-data-dir=${userDataDir}`,
winPos,
'about:blank'
];
const preferredExes = {
linux: [
'google-chrome-unstable',
'google-chrome-beta',
'google-chrome-stable'
],
darwin: ['Chrome Canary', 'Chrome'],
win32: []
};
function findChrome() {
const findingFn = chromeFinder[process.platform];
if (findingFn == null) {
throw new Error(
`Can not find chrome exe, unsupported platform - ${process.platform}`
);
}
const exes = findingFn();
const preferred = preferredExes[process.platform] || [];
let exe;
for (let i = 0; i < preferred.length; i++) {
exe = exes.find(anExe => anExe.includes(preferred[i]));
if (exe) return exe;
}
return exes[0];
}
/**
*
* @return {Promise<{chromeProcess: ChildProcess, killChrome: function(): void}>}
* @return {Promise<Browser>}
*/
async function initChrome() {
const executable = findChrome();
const userDataDir = await fs.mkdtemp(CHROME_PROFILE_PATH);
const chromeArguments = chromeArgs(userDataDir);
const chromeProcess = cp.spawn(executable, chromeArguments, {
stdio: ['ignore', 'ignore', 'pipe'],
env: process.env,
detached: process.platform !== 'win32'
const { browserWSEndpoint, closeBrowser, chromeProcess } = await launch({
args: chromeArgs
});
const maybeRemoveUDataDir = () => {
try {
fs.removeSync(userDataDir);
} catch (e) {}
};
let killed = false;
const killChrome = () => {
if (killed) {
return;
}
killed = true;
chromeProcess.kill('SIGKILL');
// process.kill(-chromeProcess.pid, 'SIGKILL')
maybeRemoveUDataDir();
};
process.on('exit', killChrome);
chromeProcess.once('exit', maybeRemoveUDataDir);
process.on('SIGINT', () => {
killChrome();
process.exit(130);
const browser = await Browser.connect(browserWSEndpoint, {
ignoreHTTPSErrors: true,
additionalDomains: { workers: true },
process: chromeProcess,
closeCallback: closeBrowser
});
process.once('SIGTERM', killChrome);
process.once('SIGHUP', killChrome);
await waitForWSEndpoint(chromeProcess, 15 * 1000);
return { chromeProcess, killChrome };
await browser.waitForTarget(t => t.type() === 'page');
return browser;
}
module.exports = initChrome;
// module.exports = initChrome
function waitForWSEndpoint(chromeProcess, timeout) {
return new Promise((resolve, reject) => {
const rl = readline.createInterface({ input: chromeProcess.stderr });
let stderr = '';
const listeners = [
criHelper.helper.addEventListener(rl, 'line', onLine),
criHelper.helper.addEventListener(rl, 'close', onClose),
criHelper.helper.addEventListener(chromeProcess, 'exit', onClose),
criHelper.helper.addEventListener(chromeProcess, 'error', onClose)
];
const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0;
function onClose() {
cleanup();
reject(new Error(['Failed to launch chrome!', stderr].join('\n')));
}
function onTimeout() {
cleanup();
reject(
new Error(
`Timed out after ${timeout} ms while trying to connect to Chrome!`
)
);
}
/**
* @param {string} line
*/
function onLine(line) {
stderr += line + '\n';
const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
if (!match) {
return;
}
cleanup();
resolve(match[1]);
}
function cleanup() {
if (timeoutId) {
clearTimeout(timeoutId);
}
criHelper.helper.removeEventListeners(listeners);
rl.close();
}
});
}

View File

@ -8,8 +8,12 @@ const gracefullShutdownTimeout = 50000;
const shutdownOnSignals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
const assetsPath = path.join(__dirname, '..', 'assets');
const httpsSandboxPath = path.join(assetsPath, 'sandbox.html');
const sandboxDirectPath = path.join(assetsPath, 'sandboxDirect.html');
const theyFoundItPath = path.join(assetsPath, 'it.html');
const testPageURL = `http://localhost:${port}/testPage.html`;
const testPageDirectURL = `http://localhost:${port}/testPageDirect.html`;
function promiseResolveReject() {
const prr = { promise: null, resolve: null, reject: null };
prr.promise = new Promise((resolve, reject) => {
@ -85,6 +89,15 @@ async function initServer() {
return fs.createReadStream(httpsSandboxPath);
}
)
.get(
'/live/20180803160549mp_/https://tests.direct.wombat.io/',
(request, reply) => {
reply
.type('text/html')
.status(200)
.send(fs.createReadStream(sandboxDirectPath));
}
)
.get(
'/live/20180803160549mp_/https://tests.wombat.io/test',
async (request, reply) => {
@ -103,7 +116,8 @@ async function initServer() {
fastify.reset();
return fastify.close();
})
.decorate('testPage', `http://localhost:${port}/testPage.html`)
.decorate('testPage', testPageURL)
.decorate('testPageDirect', testPageDirectURL)
.decorate('waitForRequest', route => {
let prr = requestSubscribers.get(route);
if (prr) return prr.promise;

View File

@ -1,29 +1,22 @@
const initChrome = require('./initChrome');
const initServer = require('./initServer');
const { CRIExtra, Browser, Events } = require('chrome-remote-interface-extra');
const testDomains = { workers: true };
const { Browser } = require('chrome-remote-interface-extra');
class TestHelper {
/**
* @param {*} t
* @param {boolean} [direct = false]
* @return {Promise<TestHelper>}
*/
static async init(t) {
const { chromeProcess, killChrome } = await initChrome();
static async init(t, direct = false) {
const browser = await initChrome();
const server = await initServer();
const { webSocketDebuggerUrl } = await CRIExtra.Version();
const client = await CRIExtra({ target: webSocketDebuggerUrl });
const browser = await Browser.create(client, {
ignoreHTTPSErrors: true,
process: chromeProcess,
additionalDomains: testDomains,
async closeCallback() {
killChrome();
}
const th = new TestHelper({
server,
browser,
t,
direct
});
await browser.waitForTarget(t => t.type() === 'page');
const th = new TestHelper({ server, client, browser, t, killChrome });
await th.setup();
return th;
}
@ -31,17 +24,12 @@ class TestHelper {
/**
* @param {TestHelperInit} init
*/
constructor({ server, client, browser, t, killChrome }) {
constructor({ server, browser, t, direct }) {
/**
* @type {fastify.FastifyInstance}
*/
this._server = server;
/**
* @type {CRIConnection}
*/
this._client = client;
/**
* @type {Browser}
*/
@ -50,13 +38,14 @@ class TestHelper {
/** @type {*} */
this._t = t;
this._killChrome = killChrome;
/** @type {Page} */
this._testPage = null;
/** @type {Frame} */
this._sandbox = null;
/** @type {boolean} */
this._direct = direct;
}
/**
@ -98,7 +87,10 @@ class TestHelper {
}
async cleanup() {
await this._testPage.goto(this._server.testPage, {
const testPageURL = this._direct
? this._server.testPageDirect
: this._server.testPage;
await this._testPage.goto(testPageURL, {
waitUntil: 'networkidle2'
});
this._sandbox = this._testPage.frames()[1];
@ -110,7 +102,8 @@ class TestHelper {
}
async ensureSandbox() {
if (!this._sandbox.url().endsWith('https://tests.wombat.io/')) {
const url = `https://tests.${this._direct ? 'direct.' : ''}wombat.io/`;
if (!this._sandbox.url().endsWith(url)) {
await this.fullRefresh();
} else {
await this.maybeInitWombat();
@ -146,9 +139,8 @@ module.exports = TestHelper;
/**
* @typedef {Object} TestHelperInit
* @property {Browser} browser
* @property {CRIConnection} client
* @property {fastify.FastifyInstance} server
* @property {*} t
* @property {function(): void} killChrome
* @property {Browser} browser
* @property {boolean} direct
*/

View File

@ -1064,3 +1064,98 @@ exports.CSS = {
}
}
};
exports.NativeFnTest = {
testFN() {
const funkyFunction = function() {};
funkyFunction.toString = function() {
throw new Error('blah');
};
return {
native: wombat.isNativeFunction(blur),
notNative: wombat.isNativeFunction(() => {}),
funkyFn: wombat.isNativeFunction(funkyFunction),
null: wombat.isNativeFunction(null),
undefined: wombat.isNativeFunction(undefined),
obj: wombat.isNativeFunction({})
};
},
expectedValue: {
native: true,
notNative: false,
funkyFn: false,
null: false,
undefined: false,
obj: false
}
};
exports.SaveSrcSetDataSrcSet = {
values: [
{ name: 'IMG', tagName: 'IMG', expected: true },
{ name: 'VIDEO', tagName: 'VIDEO', expected: true },
{ name: 'AUDIO', tagName: 'AUDIO', expected: true },
{
name: 'SOURCE with no parent',
tagName: 'SOURCE',
expected: false
},
{
name: 'SOURCE with PICTURE parent',
tagName: 'SOURCE',
parentElement: 'PICTURE',
expected: true
},
{
name: 'SOURCE with VIDEO parent',
tagName: 'SOURCE',
parentElement: 'VIDEO',
expected: true
},
{
name: 'SOURCE with AUDIO parent',
tagName: 'SOURCE',
parentElement: 'AUDIO',
expected: true
},
{
name: 'IFRAME',
tagName: 'IFRAME',
expected: false
},
{
name: 'SOURCE with DIV parent',
tagName: 'SOURCE',
parentElement: 'DIV',
expected: false
}
],
testFnSS(tagName, parentElementTagName) {
const testValue = { tagName };
if (parentElementTagName) {
testValue.parentElement = {
tagName: parentElementTagName
};
}
return wombat.isSavedSrcSrcset(testValue);
},
testFnDSS(tagName, parentElementTagName) {
const testValue = { tagName };
if (parentElementTagName) {
testValue.parentElement = {
tagName: parentElementTagName
};
}
return {
without: wombat.isSavedDataSrcSrcset(testValue),
with: wombat.isSavedSrcSrcset(
Object.assign(
{
dataset: { srcset: true }
},
testValue
)
)
};
}
};

File diff suppressed because it is too large Load Diff