Skip to content

Commit

Permalink
Warn on window and document overrides (debug builds only)
Browse files Browse the repository at this point in the history
  • Loading branch information
cvazac authored and nicjansma committed Apr 3, 2018
1 parent a98dfd3 commit f01f396
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ tests/page-templates/12-react/support/app.js
*.#*
*#*
*~
.idea/
120 changes: 120 additions & 0 deletions boomerang.js
Original file line number Diff line number Diff line change
Expand Up @@ -2069,6 +2069,126 @@ BOOMR_check_doc_domain();
}
}());

/* BEGIN_DEBUG */
/*
* This block reports on overridden functions on `window` and properties on `document` using `BOOMR.warn()`.
* To enable, add `overridden` with a value of `true` to the query string.
*/
(function() {
/**
* Checks a window for overridden functions.
*
* @param {Window} win The window object under test
*
* @returns {Array} Array of overridden function names
*/
BOOMR.checkWindowOverrides = function(win) {
var freshWindow, objects, overridden = [];
function setup() {
var iframe = d.createElement("iframe");
iframe.style.display = "none";
iframe.src = "javascript:false"; // eslint-disable-line no-script-url
d.getElementsByTagName("script")[0].parentNode.appendChild(iframe);
freshWindow = iframe.contentWindow;
objects = Object.getOwnPropertyNames(freshWindow);
}

function teardown() {
iframe.parentNode.removeChild(iframe);
}

function checkWindowObject(objectKey) {
if (isNonNative(objectKey)) {
overridden.push(objectKey);
}
}

function isNonNative(key) {
var split = key.split("."), fn = win, results = [];
while (fn && split.length) {
try {
fn = fn[split.shift()];
}
catch (e) {
return false;
}
}
return typeof fn === "function" && !isNativeFunction(fn, key);
}

function isNativeFunction(fn, str) {
if (str === "console.assert" ||
str === "Function.prototype" ||
str.indexOf("onload") >= 0 ||
str.indexOf("onbeforeunload") >= 0 ||
str.indexOf("onerror") >= 0 ||
str.indexOf("onload") >= 0 ||
str.indexOf("NodeFilter") >= 0) {
return true;
}
return fn.toString &&
!fn.hasOwnProperty("toString") &&
/\[native code\]/.test(String(fn));
}

setup();
for (var objectIndex = 0; objectIndex < objects.length; objectIndex++) {
var objectKey = objects[objectIndex];
if (objectKey === "window" ||
objectKey === "self" ||
objectKey === "top" ||
objectKey === "parent" ||
objectKey === "frames") {
continue;
}
if (freshWindow[objectKey] &&
(typeof freshWindow[objectKey] === "object" || typeof freshWindow[objectKey] === "function")) {
checkWindowObject(objectKey);

var propertyNames = [];
try {
propertyNames = Object.getOwnPropertyNames(freshWindow[objectKey]);
}
catch (e) {;}
for (var i = 0; i < propertyNames.length; i++) {
checkWindowObject([objectKey, propertyNames[i]].join("."));
}

if (freshWindow[objectKey].prototype) {
propertyNames = Object.getOwnPropertyNames(freshWindow[objectKey].prototype);
for (var i = 0; i < propertyNames.length; i++) {
checkWindowObject([objectKey, "prototype", propertyNames[i]].join("."));
}
}
}
}
return overridden;
};

/**
* Checks a document for overridden properties.
*
* @param {HTMLDocument} doc The document object under test
*
* @returns {Array} Array of overridden properties names
*/
BOOMR.checkDocumentOverrides = function(doc) {
return BOOMR.utils.arrayFilter(["readyState", "domain", "hidden", "URL", "cookie"], function(key) {
return doc.hasOwnProperty(key);
});
};

if (BOOMR.utils.getQueryParamValue("overridden") === "true" && w && w.Object && Object.getOwnPropertyNames) {
var overridden = []
.concat(BOOMR.checkWindowOverrides(w))
.concat(BOOMR.checkDocumentOverrides(d));
if (overridden.length > 0) {
BOOMR.warn("overridden: " + overridden.sort());
}
}
})();
/* END_DEBUG */

dispatchEvent("onBoomerangLoaded", { "BOOMR": BOOMR }, true);

}(window));
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@
},
"scripts": {
"test": "grunt test",
"install": "test -e node_modules/protractor/bin/webdriver-manager && node node_modules/protractor/bin/webdriver-manager update"
"install": "test -e node_modules/protractor/bin/webdriver-manager && node node_modules/protractor/bin/webdriver-manager update",
"lint": "grunt lint"
},
"repository": {
"type": "git",
Expand Down
16 changes: 16 additions & 0 deletions tests/page-templates/00-basic/09-overrides.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<%= header %>
<%= boomerangSnippet %>

<!--
this file will override the following methods:
`EventTarget.prototype.addEventListener`
`XMLHttpRequest.prototype.open`
-->
<script src="09-overrides.js" type="text/javascript"></script>

<script>
BOOMR_test.init({
testAfterOnBeacon: true
});
</script>
<%= footer %>
84 changes: 84 additions & 0 deletions tests/page-templates/00-basic/09-overrides.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*eslint-env mocha*/
/*global BOOMR_test,assert*/

describe("e2e/00-basic/09-overrides", function() {
var tf = BOOMR.plugins.TestFramework;
var windowUnderTest = window;

it("Should have sent a beacon", function() {
assert.isTrue(tf.fired_onbeacon);
});

// if window is already hijacked (phantomjs, i'm looking at you), punt
if (!((typeof document.all) !== "function" && (typeof document.all) === "function")) {
describe("clean window", function() {
it("Should return an empty array", function() {
var overrides = BOOMR.checkWindowOverrides(windowUnderTest);
assert.isTrue(BOOMR.utils.isArray(overrides));
assert.lengthOf(overrides, 0);
});
});

if (typeof Object.defineProperty === "function") {
describe("window with overrides", function() {
var _ = {};
var testMethods = [
"EventTarget.prototype.addEventListener",
"XMLHttpRequest.prototype.open"
];
before(function() {
BOOMR.utils.forEach(testMethods, function(method) {
_[method] = eval(method); // eslint-disable-line no-eval
eval(method + " = function() {};"); // eslint-disable-line no-eval
});
});
after(function() {
BOOMR.utils.forEach(testMethods, function(method) {
eval(method + " = _[method]"); // eslint-disable-line no-eval
});
});
it("Should identify non-native methods found starting at `window`", function() {
var overrides = BOOMR.checkWindowOverrides(windowUnderTest);
assert.isTrue(BOOMR.utils.isArray(overrides));
assert.lengthOf(overrides, testMethods.length);
assert.includeMembers(overrides, testMethods);
});
});
}
}

// if document is already hijacked (phantomjs, i'm looking at you), punt
if (!document.hasOwnProperty("readyState")) {
describe("clean document", function() {
it("Should return an empty array", function() {
var overrides = BOOMR.checkDocumentOverrides(document);
assert.isTrue(BOOMR.utils.isArray(overrides));
assert.lengthOf(overrides, 0);
});
});

describe("document with overrides", function() {
var _ = {};
before(function() {
BOOMR.utils.forEach(["readyState", "domain", "hidden", "URL", "cookie"], function(prop) {
_[prop] = document[prop];
Object.defineProperty(document, prop, {
value: "foo"
});
});
});
after(function() {
BOOMR.utils.forEach(Object.keys(_), function(prop) {
document[prop] = _[prop];
});
});
it("Should identify non-native properties on `document`", function() {
var overrides = BOOMR.checkDocumentOverrides(document);
assert.isTrue(BOOMR.utils.isArray(overrides));
assert.lengthOf(overrides, Object.keys(_).length);
assert.includeMembers(overrides, Object.keys(_));
});
});
}

});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*eslint-env mocha*/
/*global chai*/

describe("BOOMR.utils filter", function() {
describe("BOOMR.utils.arrayFilter()", function() {
var assert = chai.assert;

it("Should return an empty array if the function only returns false", function(){
Expand Down
53 changes: 53 additions & 0 deletions tests/unit/02-utils-foreach.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*eslint-env mocha*/
/*global chai*/

describe("BOOMR.utils.forEach()", function() {
var assert = chai.assert;

it("Should handle degenerate cases", function() {
assert.doesNotThrow(function() {
BOOMR.utils.forEach();
BOOMR.utils.forEach(null);
BOOMR.utils.forEach(true);
BOOMR.utils.forEach(false);
BOOMR.utils.forEach(0);
BOOMR.utils.forEach(1);
BOOMR.utils.forEach(123);
BOOMR.utils.forEach("");
BOOMR.utils.forEach({});
});
});

it("Should not call the callback when the array is empty", function(done) {
BOOMR.utils.forEach([], function() {
done(new Error("how dare you"));
});
done();
});

it("Should have no return value", function() {
var returnValue = BOOMR.utils.forEach([1, 2, 3], function() {});
assert.isUndefined(returnValue);
});

it("Should iterate over an array", function() {
var expected = [1, 2, 3];
var actual = [];
BOOMR.utils.forEach(expected, function(i) {
actual.push(i);
});
assert.sameMembers(expected, actual);
});

it("Should use the correct context on the callback", function(done) {
function Obj(value) {
this.value = value;
}
Obj.prototype.check = function() {
assert.strictEqual(this.value, 123);
done();
};
BOOMR.utils.forEach([0], Obj.prototype.check, new Obj(123));
});

});
3 changes: 2 additions & 1 deletion tests/unit/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
<script src="02-utils-objecttostring.js"></script>
<script src="02-utils-pluginconfig.js"></script>
<script src="02-utils-cookies.js"></script>
<script src="02-array-filter.js"></script>
<script src="02-utils-arrayfilter.js"></script>
<script src="02-utils-foreach.js"></script>
<script src="03-checkdocumentdomain.js"></script>
<script src="04-plugins-restiming.js"></script>
<script src="04-plugins-errors.js"></script>
Expand Down

0 comments on commit f01f396

Please sign in to comment.