Javascript ES6 cross-browser detection - javascript

How can I find out the browser's Javascript engine version and support to ECMAScript 6?
I'm using navigator.appVersion just to know the browser's version, but not the engine's version.

Feature detection
I suggest you to use feature detection instead of detecting the browser's engine with heuristic methods. To do this you can simply wrap some code inside a try {..} catch (e) {...} statement, or use some if (...) statements.
For example:
function check() {
if (typeof SpecialObject == "undefined") return false;
try { specialFunction(); }
catch (e) { return false; }
return true;
}
if (check()) {
// Use SpecialObject and specialFunction
} else {
// You cannot use them :(
}
Why is feature detection better than browser/engine detection?
There are multiple reasons that make, in most of the cases, feature detection the best option:
You don't have to rely on browser's version, engine or specifics, nor detect them using heuristic methods which are hard and pretty crafty to implement.
You will not fall into errors regarding browser/engine specifications detection.
You don't have to worry about browser-specific features: for example WebKit browsers have different specifications than other ones.
You can be sure that, once a feature is detected, you'll be able to use it.
These are the main reasons that IMHO make feature detection the best approach.
Feature detection + fallback
When using feature detection, a pretty smart way to work when you aren't sure which features you can/cannot use consists in several feature detections and consequent fallbacks to more basic methods (or even creation of these methods from scratch) in case the features you want to use are not supported.
A simple example of feature detection with fallback may be applied to the window.requestAnimationFrame feature, which is not supported by all the browsers, and has several different prefixes depending on the browser you're working on. In this case, you can easily detect and fallback like this:
requestAnimationFrame =
window.requestAnimationFrame // Standard name
|| window.webkitRequestAnimationFrame // Fallback to webkit- (old versions of Chrome or Safari)
|| window.mozRequestAnimationFrame // Fallback to moz- (Mozilla Firefox)
|| false; // Feature not supported :(
// Same goes for cancelAnimationFrame
cancelAnimationFrame = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || false;
if (!requestAnimationFrame) {
// Not supported? Build it by yourself!
requestAnimationFrame = function(callback) {
return setTimeout(callback, 0);
}
// No requestAnim. means no cancelAnim. Built that too.
cancelAnimationFrame = function(id) {
clearTimeout(id);
}
}
// Now you can use requestAnimationFrame
// No matter which browser you're running
var animationID = requestAnimationFrame(myBeautifulFunction);
ECMAScript 6 (Harmony) features detection
Now, coming to the real problem: if you want to detect the support to ES6, you'll not be able to behave like I said above, because a relevant range of ES6 features is based on new syntaxes and private words, and will throw a SyntaxError if used in ES5, which means that writing a script which contains both ES5 and ES6 is impossible!
Here is an example to demonstrate this issue; the below snippet won't work, and it will be blocked before execution because contains illegal syntax.
function check() {
"use strict";
try { eval("var foo = (x)=>x+1"); }
catch (e) { return false; }
return true;
}
if (check()) {
var bar = (arg) => { return arg; }
// THIS LINE will always throw a SyntaxError in ES5
// even before checking for ES6
// because it contains illegal syntax.
} else {
var bar = function(arg) { return arg; }
}
Now, since that you cannot both check and execute ES6 conditionally in the same script, you'll have to write two different scripts: one which only uses ES5, and another one which includes ES6 features. With two different scripts you'll be able to import the ES6 one only if it is supported, and without causing SyntaxErrors to be thrown.
ES6 detection and conditional execution example
Now let's make a more relatable example, and let's say you want to use these features in your ES6 script:
The new Symbol objects
Classes built with the class keyword
Arrow ((...)=>{...}) functions
NOTE: feature detection of newly introduced syntaxes (like arrow functions) can only be done using the eval() function or other equivalents (e.g. Function()), because writing invalid syntax will stop the script before its execution. This is also the reason why you cannot use if statements to detect classes and arrow functions: these features are regarding keywords and syntax, so an eval(...) wrapped inside a try {...} catch (e) {...} block will work fine.
So, coming to the real code:
HTML Markup:
<html>
<head>
<script src="es5script.js"></script>
</head>
<body>
<!-- ... -->
</body>
</html>
Code in your es5script.js script:
function check() {
"use strict";
if (typeof Symbol == "undefined") return false;
try {
eval("class Foo {}");
eval("var bar = (x) => x+1");
} catch (e) { return false; }
return true;
}
if (check()) {
// The engine supports ES6 features you want to use
var s = document.createElement('script');
s.src = "es6script.js";
document.head.appendChild(s);
} else {
// The engine doesn't support those ES6 features
// Use the boring ES5 :(
}
Code in your es6script.js:
// Just for example...
"use strict";
class Car { // yay!
constructor(speed) {
this.speed = speed;
}
}
var foo = Symbol('foo'); // wohoo!
var bar = new Car(320); // blaze it!
var baz = (name) => { alert('Hello ' + name + '!'); }; // so cool!
Browser/engine detection
Like I said above, browser and engine detection are not the best practices when programming some JavaScript script. I'm gonna give you some background on this topic, just not to leave my words as a "random personal opinion".
Quoting from the MDN Documentation [link]:
When considering using the user agent string to detect which browser is being used, your first step is to try to avoid it if possible. Start by trying to identify why you want to do it.
[...] Are you trying to check for the existence of a specific feature?
Your site needs to use a specific Web feature that some browsers don't yet support, and you want to send those users to an older Web site with fewer features but that you know will work. This is the worst reason to use user agent detection, because odds are eventually all the other browsers will catch up. You should do your best to avoid using user agent sniffing in this scenario, and do feature detection instead.
Also, you're saying you use navigator.appVersion, but consider using another approach, because that one, together with many other navigator properties, is deprecated, and doesn't always behave like you think.
So, quoting from the MDN Documentation [link] again:
Deprecated: this feature has been removed from the Web standards. Though some browsers may still support it, it is in the process of being dropped. Do not use it in old or new projects. Pages or Web apps using it may break at any time.
Note: Do not rely on this property to return the correct browser version. In Gecko-based browsers (like Firefox) and WebKit-based browsers (like Chrome and Safari) the returned value starts with "5.0" followed by platform information. In Opera 10 and newer the returned version does not match the actual browser version, either.

Browser vendors that support ES6 modules now provide an easy way to do feature detection:
...
<head>
<script nomodule>window.nomodules = true;</script>
<script>console.log(window.nomodules)</script>
</head>
...
The script with the nomodule attribute will not be excuted by browsers which support <script type="module" ...>
You can also inject the script like this:
const script = document.createElement('script');
script.setAttribute('nomodule', '');
script.innerHTML = 'window.nomodules = true;';
document.head.insertBefore(script, document.head.firstChild);
script.remove();

As Marco Bonelli said, the best way to detect ECMAScript 6 language syntax is to use eval();. If the call does not throw an error, "all other" features are supported, but I recommend Function();.
function isES6()
{
try
{
Function("() => {};"); return true;
}
catch(exception)
{
return false;
}
}
demo: https://jsfiddle.net/uma4Loq7/

No eval ES6 feature detection
You can do it without using eval - just insert the detection code in its own script block and make a global variable assignment at the end. The variable assignment will not run if any error occurs in the script block.
<script>
window.isES6 = false;
</script>
<script>
// Arrow functions support
() => { };
// Class support
class __ES6FeatureDetectionTest { };
// Object initializer property and method shorthands
let a = true;
let b = {
a,
c() { return true; },
d: [1,2,3],
};
// Object destructuring
let { c, d } = b;
// Spread operator
let e = [...d, 4];
window.isES6 = true;
</script>
<script>
document.body.innerHTML += 'isES6: ' + window.isES6;
</script>
https://jsfiddle.net/s5tqow91/2/
Please note that there are many ES6 features, and checking only one does not guarantee you are covered. (The above code doesn't cover everything either, it's just what I think are the features that I use most often).
Why no eval?
The main reason is security and it's not that calling eval for feature detection is insecure by itself. It's that ideally you should disallow eval with Content Security Policy so it cannot be used at all - which greatly decreases attack surface. But if your own code uses eval, you cannot do that.

Detect devicePixelRatio which is a special property in WebKit.
Detect javaEnabled function's implement.
(function() {
var v8string = 'function%20javaEnabled%28%29%20%7B%20%5Bnative%20code%5D%20%7D';
var es6string = 'function%20javaEnabled%28%29%20%7B%0A%20%20%20%20%5Bnative%20code%5D%0A%7D';
if (window.devicePixelRatio) //If WebKit browser
{
var s = escape(navigator.javaEnabled.toString());
if (s === v8string) {
alert('V099787 detected');
} else if (s === es6string) {
alert('ES6 detected')
} else {
alert('JSC detected');
}
} else {
display("Not a WebKit browser");
}
function display(msg) {
var p = document.createElement('p');
p.innerHTML = msg;
document.body.appendChild(p);
}
})()

For now there's not a exact way to detect ES6, but if you test its features in the current browser, you can determine if the engine is ES6. My esx library detects the ECMAScript version by doing syntax tests and methods check. For know it can detect ECMAScript 3, 5, 6 and 7 (ES7 not tested, but should work), if no ECMAScript test matched, it gives null as result.
Example using my library:
if (esx.detectVersion() >= 6) {
/* We're in ES6 or above */
}

As Damian Yerrick has mentioned, the use of eval() or Function() is incompatible with a Content Security Policy that does not specify 'unsafe-eval'.
If the browser supports Worker then you can detect support for any ES6 syntax by implementing that syntax in a worker and checking for error or successs eg to detect support for arrow functions:
worker.js
// If ES6 arrow functions are supported then the worker listener will receive true, otherwise it will receive an error message
(() => {
postMessage(true);
})();
index.js
if (typeof (Worker) !== "undefined") {
var myWorker = new Worker('worker.js');
myWorker.onmessage = function (e) {
// arrow functions must be supported since we received message from the worker arrow function
}
myWorker.onerror = function (e) {
// the worker triggered an error so arrow function not supported (could explicitly check message for syntax error)
}
}

This function returns true in Chrome 98.0.4758.80 and Firefox 97.0.2 (just tested). It may not work on other browsers and previous versions of Chrome/Firefox (false negative results)
function hasAsyncSupport () {
return Object.getPrototypeOf(async function() {}).constructor.toString().includes('Async')
}

The best way, I think, is to simply write all ES6 scripts as modules, and use nomodule script tags for any fallbacks. Trying to code inline detection is not needed.
Of course, this begs the question of how to detect new syntax from post-ES6 versions, such as the "??" and "?." operators.

Put the incompatible syntax code, such as containing arrow functions, in it's own script block and polyfill it with compatible syntax code.
<script>
// This script block should not compile on incompatible browsers,
// leaving the function name undefined.
// It can then be polyfilled with a function containing compatible syntax code.
function fame() {
/* incompatible syntax code such as arrow functions */
}
</script>
<script>
if (typeof fame !== "function") {
// alert("polyfill: fame");
function fame() {
/* compatible syntax code */
}
}
</script>
<script>
// main code
fame();
</script>

Related

How to disable console.log messages based on criteria from specific javascript source (method, file) or message contents

I am working on project that uses quite a few js libraries and one of them is outputting awful lot into console, it is polluting the airwaves so bad that it makes it hard to debug....
I know how to disable logging completely by overriding console.log with this,
(function (original) {
console.enableLogging = function () {
console.log = original;
};
console.disableLogging = function () {
console.log = function () {};
};
})(console.log);
but how do it do that per source(file/url) of where message originated?
Preamble
The beginning discusses how stuff works in general. If you just care for the code, skip Introduction and scroll to the Solution heading.
Introduction
Problem:
there is a lot of console noise in a web application. A significant amount of that noise is coming from third party code which we do not have access to. Some of the log noise might be coming from our code, as well.
Requirement:
reduce the noise by stopping the log. Some logs should still be kept and the decision about those should be decoupled from the code that is doing the logging. The granularity needed is "per-file". We should be able to choose which files do or do not add log messages. Finally, this will not be used in production code.
Assumption: this will be ran in a developer controlled browser. In that case, I will not focus on backwards compatibility.
Prior work:
First off logging can be enabled/disabled globally using this
(function (original) {
console.enableLogging = function () {
console.log = original;
};
console.disableLogging = function () {
console.log = function () {};
};
})(console.log);
(code posted in the question but also here for reference)
However, that does not allow for any granularity.
This could be modified to work on only specific modules but that cannot be done for third party code.
A mixed approach would be to disable logging globally but enable it in each of our modules. Problem there is that we have to modify each of our files and we will not get some potentially useful external messages.
A logging framework can be used but it might be an overkill. Although, to be honest, that's what I'd go for, I think, but it may need some integration into the product.
So, we need something light-weight-ish that has some configuration and does not need to be pretty.
Proposal:
The Loginator (title subject to change)
Let's start with the basics - we already know we can override the global log function. We'll take that and work with it. But first, let's recognise that the console object supports more than just .log. There could be various logging functions used. So-o-o, let's disable all of them.
Silence everything
//shorthand for further code.
function noop() {}
const savedFunctions = Object.keys(console)
.reduce((memo, key) => {
if(typeof console[key] == "function") {
//keep a copy just in case we need it
memo[key] = console[key];
//de-fang any functions
console[key] = noop;
}
return memo;
},
{});
console.log("Hello?");
console.info("Hello-o-o-o?");
console.warn("Can anybody hear me?");
console.error("I guess there is nobody there...");
savedFunctions.log("MUAHAHAHA!")
This can obviously be improved but it showcases how any and ll logging can be stopped. In reality, console.error should probably be left and console.warn might be also useful. But this is not the be-all-and-end-all solution.
Next, since we can override console functionality...why not supply our own?
Custom logging
const originalLog = console.log;
console.log = function selectiveHearing() {
if (arguments[0].indexOf("die") !== -1) {
arguments[0] = "Have a nice day!";
}
return originalLog.apply(console, arguments)
}
console.log("Hello.");
console.log("My name is Inigo Montoya.");
console.log("You killed my father.");
console.log("Prepare to die.");
That is all the tools we need to roll our own mini-logging framework.
How to do selective logging
The only thing missing is to determine which file something is coming from. We just need a stack trace.
// The magic
console.log(new Error().stack);
/* SAMPLE:
Error
at Object.module.exports.request (/home/vagrant/src/kumascript/lib/kumascript/caching.js:366:17)
at attempt (/home/vagrant/src/kumascript/lib/kumascript/loaders.js:180:24)
at ks_utils.Class.get (/home/vagrant/src/kumascript/lib/kumascript/loaders.js:194:9)
at /home/vagrant/src/kumascript/lib/kumascript/macros.js:282:24
at /home/vagrant/src/kumascript/node_modules/async/lib/async.js:118:13
at Array.forEach (native)
at _each (/home/vagrant/src/kumascript/node_modules/async/lib/async.js:39:24)
at Object.async.each (/home/vagrant/src/kumascript/node_modules/async/lib/async.js:117:9)
at ks_utils.Class.reloadTemplates (/home/vagrant/src/kumascript/lib/kumascript/macros.js:281:19)
at ks_utils.Class.process (/home/vagrant/src/kumascript/lib/kumascript/macros.js:217:15)
*/
(Relevant bit copied here.)
True, there are some better ways to do it but not a lot. It would either require a framework or it's browser specific - error stacks are not officially supported but they work in Chrome, Edge, and Firefox. Also, come on - it's literally one line - we want simple and don't mind dirty, so I'm happy for the tradeoff.
Solution
Putting it all together. Warning: Do NOT use this in production
(function(whitelist = [], functionsToPreserve = ["error"]) {
function noop() {}
//ensure we KNOW that there is a log function here, just in case
const savedFunctions = { log: console.log }
//proceed with nuking the rest of the chattiness away
Object.keys(console)
.reduce((memo, key) => {
if(typeof console[key] == "function" && functionsToPreserve.indexOf(key) != -1 ) {
memo[key] = console[key];
console[key] = noop;
}
return memo;
},
savedFunctions); //<- it's a const so we can't re-assign it. Besides, we don't need to, if we use it as a seed for reduce()
console.log = function customLog() {
//index 0 - the error message
//index 1 - this function
//index 2 - the calling function, i.e., the actual one that did console.log()
const callingFile = new Error().stack.split("\n")[2];
if (whitelist.some(entry => callingFile.includes(entry))) {
savedFunctions.log.apply(console, arguments)
}
}
})(["myFile.js"]) //hey, it's SOMEWHAT configurable
Or a blacklist
(function(blacklist = [], functionsToPreserve = ["error"]) {
function noop() {}
//ensure we KNOW that there is a log function here, just in case
const savedFunctions = {
log: console.log
}
//proceed with nuking the rest of the chattiness away
Object.keys(console)
.reduce((memo, key) => {
if (typeof console[key] == "function" && functionsToPreserve.indexOf(key) != -1) {
memo[key] = console[key];
console[key] = noop;
}
return memo;
},
savedFunctions); //<- it's a const so we can't re-assign it. Besides, we don't need to, if we use it as a seed for reduce()
console.log = function customLog() {
//index 0 - the error message
//index 1 - this function
//index 2 - the calling function, i.e., the actual one that did console.log()
const callingFile = new Error().stack.split("\n")[2];
if (blacklist.some(entry => callingFile.includes(entry))) {
return;
} else {
savedFunctions.log.apply(console, arguments);
}
}
})(["myFile.js"])
So, this is a custom logger. Sure, it's not perfect but it will do the job. And, hey, since the whitelisting is a bit loose, it could be turned to an advantage:
to whitelist a bunch of files that share a substring, say, all myApp can include myApp1.js, myApp2.js, and myApp3.js.
although if you want specific files, you can just pass the full name, including extension. I doubt there would be a bunch of duplicate filenames.
finally, the stack trace will include the name of the calling function, if any, so you can actually just pass that and that will whitelist on per-function basis. However, it relies on the function having a name and it's more likely for function names to clash, so use with care
Other than that, there can certainly be improvements but that is the basis of it. The info/warn methods can also be overriden, for example.
So, this, if used, should only be in dev builds. There are a lot of ways to make it not go into production, so I won't discuss them but here is one thing I can mention: you can also use this anywhere if you save it as a bookmarklet
javascript:!function(){function c(){}var a=arguments.length<=0||void 0===arguments[0]?[]:arguments[0],b=arguments.length<=1||void 0===arguments[1]?["error"]:arguments[1],d={log:console.log};Object.keys(console).reduce(function(a,d){return"function"==typeof console[d]&&b.indexOf(d)!=-1&&(a[d]=console[d],console[d]=c),a},d),console.log=function(){var c=(new Error).stack.split("\n")[2];a.some(function(a){return c.includes(a)})&&d.log.apply(console,arguments)}}(["myFile.js"]);
This is it minified (although I passed it through Babel first, to use ES5 minification) and still configurable, to an extent, as you can change the very end where you can pass the whitelist. But other than that, it will work the same and is completely decoupled from the codebase. It will not run at pageload but if that's needed you can either use this as a userscript (still decoupled) or include it before other JS files in dev/debug builds only.
A note here - this will work in Chrome, Edge and Firefox. It's all the latest browsers, so I assume a developer will use at least one of them. The question is tagged as Chrome but I decided to widen the support. A Chrome only solution could work slightly better but it's not really a big loss of functionality.
I was as troubled as you. This is my approach. https://github.com/jchnxu/guard-with-debug
Simple usage:
localStorage.debug = [
'enable/console/log/in/this/file.ts',
'enable/console/log/in/this/folder/*',
'-disable/console/log/in/this/file.ts',
'-disable/console/log/in/this/folder/*',
// enable all
'*',
].join(',');
The benefit: it's zero-runtime.
Disclaimer: I am the author of this tiny utility
It work in chrome:
...index.html
<html>
<body>
<script>
(function(){
var original = console.log;
console.log = function(){
var script = document.currentScript;
alert(script.src);
if(script.src === 'file:///C:/Users/degr/Desktop/script.js') {
original.apply(console, arguments)
}
}
})();
console.log('this will be hidden');
</script>
<script src="script.js"></script>
</body>
</html>
...script.js
console.log('this will work');
Console.log does not work from index.html, but work from script.js. Both files situated on my desctop.
I've found these settings in the latest (July 2020) Chrome DevTools console to be helpful:
DevTools | Console | (sidebar icon) | user messages
DevTools | Console | (gear icon) | Select context only
DevTools | Console | (gear icon) | Hide network
I like (1) most, I only see the messages from "my" code. (2) hides messages from my iframe.
If it's an option to modify file, you can set a flag at top of file for disabling logs for that:
var DEBUG = false;
DEBUG && console.log("cyberpunk 2077");
To disable logs for all js files, put it once at top of any js file:
var DEBUG = false;
if (!DEBUG) {
console.log = () => {};
}
This is not pretty but will work.
Put something like this in your file before the <script> tag of the "bad" library :
<script>function GetFile(JSFile) {
var MReq = new XMLHttpRequest();
MReq.open('GET', JSFile, false);
MReq.send();
eval(MReq.responseText.replace(/console.log\(/g,"(function(){})("));
}</script>
Then replace the tag
<script src="badLib.js">
With:
GetFile("badLib.js")
Only for short time debugging.

Object doesn't support this property or method in IE8: _normalizeServiceName

I'm upgrading my app from .NET 4.0 to 4.5, and upgrading Breeze.js from 1.4.17 to 1.5.4. Things seem to be working well in IE11 and other browsers, but in IE8 (or in IE11 in IE8 mode) I am getting an "Object doesn't support this property or method" error related to the _normalizeServiceName method in this section of code:
function updateWithConfig(obj, config) {
if (config) {
assertConfig(config)
.whereParam("serviceName").isOptional()
.whereParam("adapterName").isString().isOptional()
.whereParam("uriBuilderName").isString().isOptional()
.whereParam("hasServerMetadata").isBoolean().isOptional()
.whereParam("jsonResultsAdapter").isInstanceOf(JsonResultsAdapter).isOptional()
.whereParam("useJsonp").isBoolean().isOptional()
.applyAll(obj);
obj.serviceName = obj.serviceName && DataService._normalizeServiceName(obj.serviceName); // <<< Error here
obj.adapterInstance = obj.adapterName && __config.getAdapterInstance("dataService", obj.adapterName);
obj.uriBuilder = obj.uriBuilderName && __config.getAdapterInstance("uriBuilder", obj.uriBuilderName);
}
return obj;
}
I can see that the _normalizeServiceName method is defined right after updateWithConfig:
ctor._normalizeServiceName = function (serviceName) {
serviceName = serviceName.trim();
if (serviceName.substr(-1) !== "/") {
return serviceName + '/';
} else {
return serviceName;
}
};
If I trace through where DataService is defined, ctor does have the _normalizeServiceName method defined when it is returned, but by the time updateWithConfig is called it is missing from DataService.
The error occurs when I create a new EntityManager:
this.manager = new breeze.EntityManager(appRoot + "breeze/myapp");
The Breeze website seems to say that IE8 is still supported. I have the ES5 Shim/Sham scripts referenced in a conditional comment:
<!--[if lt IE 9]>
<script src="/myapp/js/respond.js"></script>
<script src="/myapp/js/es5-shim.js"></script>
<script src="/myapp/js/es5-sham.js"></script>
<script src="/myapp/js/json3.js"></script>
<![endif]-->
So, is IE8 still supported in Breeze? Did I miss something I need to update in my code when going from Breeze 1.4.x to 1.5.x (the only thing I changed was related to the Promise API seeming to change)? Or is this a bug in Breeze?
Perhaps not an answer to "does Breeze support IE8", but I thought I would document what I did to get things running again, in case it helps anyone else.
The first issue was that IE8 doesn't like the named constructor functions (if that's even the right terminology) introduced in this commit to breeze.js. To get around that, I added a replace step to my gulpfile.js to remove the function names:
gulp.task("breeze", function () {
var js = gulp
.src(src.bower + "breeze-client/build/breeze.debug.js")
.pipe(replace(/var ctor = function (\w+)/g, "var ctor = function "))
.pipe(rename("breeze.js"))
.pipe(gulp.dest(dest.js));
return merge(js);
});
Also, since I am using TypeScript, the current typings for Breeze define an IPromise interface (changed from Q.Promise in this commit, presumably to support Angular) that uses method names that IE8 doesn't like (catch, finally) and doesn't define the ES3-friendly aliases (fail, fin). I added my own definitions, and also defined the Q done() method:
declare module breeze.promises {
// IE8 (and other ES3 browsers?) don't like .catch and .finally
// Also define Q's .done method.
interface IPromise<T> {
done<U>(): IPromise<U>;
fail<U>(onRejected: (reason: any) => U): IPromise<U>;
fail<U>(onRejected: (reason: any) => IPromise<U>): IPromise<U>;
fin(finallyCallback: () => any): IPromise<T>;
}
}
Finally, I had updated es5-shim to v4.5.7 as part of my upgrade, which seems to break IE8 too, although I have no idea how/why at the moment. I have no idea what version I was using before (can't find a version number in the .js file) since I had manually copied it in a long time ago. I had to go all the way back to v4.1.7 to find a version that worked, so presumably it's something in v4.1.8 that breaks IE8.
With all that, things seem to be working in IE8 again!

Testing functionality based on HTML5 DOM-methods

I'm wondering how to test functionality based on HTML5 DOM-methods. Like the new dialog-API for example.
Consider the following function;
function show(dialog)
{
if(typeof(dialog.showModal) !== "function")
throw new Error("Update your browser");
dialog.showModal();
}
I want to make sure that
an error is thrown when the method is not available
dialog.showModal is called if available
I'm currently running karma + jasmine with the firefox and chrome launchers but none of them seems to support the dialog-API.
Can I shim this in some kind of way? Can I create a function on all dialog DOM nodes?
I could do the following
var modal = document.createElement("dialog");
Object.defineProperty(modal, "showModal", {
value: function()
{
console.log("hello world!");
}
});
however, that isn't carried over to other instances of a dialog.
This is a very simplified example of what I'm trying to achieve.
This is what polyfills are: an attempt to shim support for newer specs into browsers that don't support them. What you're talking about is essentially creating a polyfill for the <dialog> element.
Some older ECMAScript 6 specs specified a function to test if something existed and was a function, but this seems to have been scrapped in more recent specs. This is a "polyfill" for that (in whatever way that term can apply to a scrapped spec), both as an example of polyfilling and as a tool we'll use to explore the concept further:
(function () {
"use strict";
var ops = Object.prototype.toString;
Function.isFunction = function(o) {
return ops.call(o) === "[object Function]";
};
}());
Note that we have to cache the original Object.prototype.toString: most JavaScript runtimes override it in Function.prototype.toString, and we really need to be calling the original. This should give you an idea of how fiddly and error-prone polyfilling can be: even in a case like this where the code is short and simple, nuances like using Object.prototype.toString.call(o) instead of o.toString() are still critical.
Anyway, once you've got this function, it becomes much easier to test for things like modal support:
(function () {
"use strict";
var test = document.createElement("dialog");
if (!Function.isFunction(test.showModal)) {
throw new Error("HTMLDialogElement.showModal() is not supported");
};
}());
To modify all dialog element instances, you would extend the prototype that most immediately pertains to that object. In the case of the HTML dialog spec, that's HTMLDialogElement. You could do something like this:
(function () {
"use strict";
var HTMLDialogElement = HTMLDialogElement; //undefined if unsupported
if (!Function.isFunction(HTMLDialogElement)) {
// No native dialog support
throw new Error("No support for <dialog> elements");
}
if (!Function.isFunction(HTMLDialogElement.prototype.showModal)) {
// The interface exists, but showModal isn't supported
HTMLDialogElement.prototype.showModal = function () {
console.log("Hello world!");
};
}
}());
If there's no support for the prototype either, then things get trickier. An unknown element doesn't get a distinctive prototype; it's just an HTMLElement (like all other elements, eventually). So to make a function work for <dialog> elements in an unsupported environment, you'd have to add it to all elements. Still, you can sort of make it work:
(function () {
"use strict";
HTMLElement.prototype.showModal = function () {
if (this.tagName !== "DIALOG") {
throw new TypeError("showModal() called on non-dialog");
}
console.log("Hello world!");
};
}());
Most polyfills work based on compromises like this: they don't support the spec perfectly, but do well enough to support common use cases.
Be warned: polyfilling extends native objects, and this is commonly considered bad practice. Most people who call it bad practice will make an exception for polyfills, but the rule is still considered good even though polyfills are so useful. That should give you an idea of how complex and error-prone the task can be. This is black magic: not a task to be undertaken lightly.

How to Test an optional member of an object?

What is the best technique to test an optional object member. Right now we are prefacing the expect statements with an if:
if(object.member) expect(object).to.have.a.property('member').that.is.a('string');
but there must be a method that is more inline stylistically. E.g.
expect(object).to.have.an.optional.property('member').that.is.a('string');
or (adding would as an empty chain, for readability):
expect(object).to.have.an.optional.property('member').that.would.be.a('string');
or (moving the optional to provide an alternative version of expect):
optionally.expect(object).to.have.a.property('member').that.is.a('string');
update - I started to write this code (new to chai) to see if I could accomplish what I was targeting, so I added a small plugin:
module.exports = function(chai, utils) {
var Assertion = chai.Assertion
, i = utils.inspect
, flag = utils.flag;
var OPTIONAL_FLAG = 'chai-optional/option'
Assertion.addProperty('optional', function() {
flag(this, OPTIONAL_FLAG, true)
return this;
})
Assertion.overwriteMethod('property', function (_super) {
return function assertProperty (propertyName) {
if (flag(this, OPTIONAL_FLAG)) {
flag(this, OPTIONAL_FLAG, false) ;
var obj = this._obj;
var isPropertyPresent = (obj[propertyName]) ? true : false ;
if(isPropertyPresent) {
return _super.apply(this, arguments);
}
} else {
_super.apply(this, arguments);
}
};
});
Assertion.addProperty('would', function () {
return this;
});
};
WIth usage:
it('could be null or have a value', function(done){
var objWithout = {}
var objWith = {}
objWith.someProperty = 'blah'
expect(objWith).to.have.optional.property('someProperty').that.would.be.a('string');
expect(objWithout).to.have.optional.property('someProperty').that.would.be.a('string');
return done();
})
The current problem even when the property isn't present, the control of the function ends - but the evaluation chain continues. I need to end the evaluation with out a failing assertion - is this possible?
update either solution (simplistic solution):
var either = function(firstCondition){
var returnObject = {}
try{
firstCondition()
returnObject.or = function(secondCondition){ return }
} catch(e) {
returnObject.or = function(secondCondition){ return secondCondition() }
}
return returnObject ;
}
module.exports = either
I think the implementation is a little clunky - but fat arrow functions will hap thin out some of the syntax. So here is waiting on that!
The current problem even when the property isn't present, the control of the function ends - but the evaluation chain continues. I need to end the evaluation with out a failing assertion - is this possible?
After having read about chai's plugin guide I would have used a similar approach with a flag. However, I have reached the same conclusion - you cannot simply stop a chain.
A possibility I though of would be not only to implement new properties and a new flag, but to overwrite the assert method itself - to not throw when the OPTIONAL_FLAG flag on the current Assertion object is set. However, the chance to destroy everything or to miss an edge case is too hight.
After all, I don't think it's a good idea. Citing from this "confusing syntax" issue:
I think the misunderstanding comes from your expectation that Chai
follows most/all English grammar rules. Unfortunately, English grammar
has way too many rules (and exceptions to those rules) for it to be a
reasonable undertaking to implement.
The challenge with designing Chai's chain-able assertions is finding
the balance between being expressive and concise. Even if full grammar
wasn't a daunting task to implement and document, it would make the
API less concise, which is not good for a testing environment.
RULE: Any "flag" which modifies the behavior of an assertion (negation
not or inclusion include/contain , etc...), once set to true
should remain true until the end of the chain.
This means it is impossible to implement something like an .or operator as well.
What is possible though would be to implement something like
either(function(){
expect(object).not.to.have.a.property('member');
}).or(function(){
expect(object).to.have.a.property('member').that.is.a('string');
});
Maybe one can build a more appealing syntax on that.

Is it possible to sandbox JavaScript running in the browser?

I'm wondering if it's possible to sandbox JavaScript running in the browser to prevent access to features that are normally available to JavaScript code running in an HTML page.
For example, let's say I want to provide a JavaScript API for end users to let them define event handlers to be run when "interesting events" happen, but I don't want those users to access the properties and functions of the window object. Am I able to do this?
In the simplest case, let's say I want to prevent users calling alert. A couple of approaches I can think of are:
Redefine window.alert globally. I don't think this would be a valid approach because other code running in the page (i.e., stuff not authored by users in their event handlers) might want to use alert.
Send the event handler code to the server to process. I'm not sure that sending the code to the server to process is the right approach, because the event handlers need to run in the context of the page.
Perhaps a solution where the server processes the user defined function and then generates a callback to be executed on the client would work? Even if that approach works, are there better ways to solve this problem?
Google Caja is a source-to-source translator that "allows you to put untrusted third-party HTML and JavaScript inline in your page and still be secure."
Have a look at Douglas Crockford's ADsafe:
ADsafe makes it safe to put guest code (such as third party scripted advertising or widgets) on any web page. ADsafe defines a subset of JavaScript that is powerful enough to allow guest code to perform valuable interactions, while at the same time preventing malicious or accidental damage or intrusion. The ADsafe subset can be verified mechanically by tools like JSLint so that no human inspection is necessary to review guest code for safety. The ADsafe subset also enforces good coding practices, increasing the likelihood that guest code will run correctly.
You can see an example of how to use ADsafe by looking at the template.html and template.js files in the project's GitHub repository.
I created a sandboxing library called jsandbox that uses web workers to sandbox evaluated code. It also has an input method for explicitly giving sandboxed code data it wouldn't otherwise be able to get.
The following is an example of the API:
jsandbox
.eval({
code : "x=1;Math.round(Math.pow(input, ++x))",
input : 36.565010597564445,
callback: function(n) {
console.log("number: ", n); // number: 1337
}
}).eval({
code : "][];.]\\ (*# ($(! ~",
onerror: function(ex) {
console.log("syntax error: ", ex); // syntax error: [error object]
}
}).eval({
code : '"foo"+input',
input : "bar",
callback: function(str) {
console.log("string: ", str); // string: foobar
}
}).eval({
code : "({q:1, w:2})",
callback: function(obj) {
console.log("object: ", obj); // object: object q=1 w=2
}
}).eval({
code : "[1, 2, 3].concat(input)",
input : [4, 5, 6],
callback: function(arr) {
console.log("array: ", arr); // array: [1, 2, 3, 4, 5, 6]
}
}).eval({
code : "function x(z){this.y=z;};new x(input)",
input : 4,
callback: function(x) {
console.log("new x: ", x); // new x: object y=4
}
});
An improved version of RyanOHara's web workers sandbox code, in a single file (no extra eval.js file is necessary).
function safeEval(untrustedCode)
{
return new Promise(function (resolve, reject)
{
var blobURL = URL.createObjectURL(new Blob([
"(",
function ()
{
var _postMessage = postMessage;
var _addEventListener = addEventListener;
(function (obj)
{
"use strict";
var current = obj;
var keepProperties =
[
// Required
'Object', 'Function', 'Infinity', 'NaN', 'undefined', 'caches', 'TEMPORARY', 'PERSISTENT',
// Optional, but trivial to get back
'Array', 'Boolean', 'Number', 'String', 'Symbol',
// Optional
'Map', 'Math', 'Set',
];
do
{
Object.getOwnPropertyNames(current).forEach(function (name)
{
if (keepProperties.indexOf(name) === -1)
{
delete current[name];
}
});
current = Object.getPrototypeOf(current);
}
while (current !== Object.prototype)
;
})(this);
_addEventListener("message", function (e)
{
var f = new Function("", "return (" + e.data + "\n);");
_postMessage(f());
});
}.toString(),
")()"],
{type: "application/javascript"}));
var worker = new Worker(blobURL);
URL.revokeObjectURL(blobURL);
worker.onmessage = function (evt)
{
worker.terminate();
resolve(evt.data);
};
worker.onerror = function (evt)
{
reject(new Error(evt.message));
};
worker.postMessage(untrustedCode);
setTimeout(function ()
{
worker.terminate();
reject(new Error('The worker timed out.'));
}, 1000);
});
}
Test it:
https://jsfiddle.net/kp0cq6yw/
var promise = safeEval("1+2+3");
promise.then(function (result) {
alert(result);
});
It should output 6 (tested in Chrome and Firefox).
As mentioned in other responces, it's enough to jail the code in a sandboxed iframe (without sending it to the server-side) and communicate with messages.
I would suggest to take a look at a small library I created mostly because of the need to providing some API to the untrusted code, just like as described in the question: there's an opportunity to export the particular set of functions right into the sandbox where the untrusted code runs. And there's also a demo which executes the code submitted by a user in a sandbox:
http://asvd.github.io/jailed/demos/web/console/
I think that js.js is worth mentioning here. It's a JavaScript interpreter written in JavaScript.
It's about 200 times slower than native JavaScript, but its nature makes it a perfect sandbox environment. Another drawback is its size – almost 600 KB, which may be acceptable for desktops in some cases, but not for mobile devices.
All the browser vendors and the HTML5 specification are working towards an actual sandbox property to allow sandboxed iframes -- but it's still limited to iframe granularity.
In general, no degree of regular expressions, etc. can safely sanitise arbitrary user provided JavaScript as it degenerates to the halting problem :-/
An ugly way, but maybe this works for you:
I took all the globals and redefined them in the sandbox scope, as well I added the strict mode so they can't get the global object using an anonymous function.
function construct(constructor, args) {
function F() {
return constructor.apply(this, args);
}
F.prototype = constructor.prototype;
return new F();
}
// Sanboxer
function sandboxcode(string, inject) {
"use strict";
var globals = [];
for (var i in window) {
// <--REMOVE THIS CONDITION
if (i != "console")
// REMOVE THIS CONDITION -->
globals.push(i);
}
globals.push('"use strict";\n'+string);
return construct(Function, globals).apply(inject ? inject : {});
}
sandboxcode('console.log( this, window, top , self, parent, this["jQuery"], (function(){return this;}()));');
// => Object {} undefined undefined undefined undefined undefined undefined
console.log("return of this", sandboxcode('return this;', {window:"sanboxed code"}));
// => Object {window: "sanboxed code"}
https://gist.github.com/alejandrolechuga/9381781
An independent JavaScript interpreter is more likely to yield a robust sandbox than a caged version of the built-in browser implementation.
Ryan has already mentioned js.js, but a more up-to-date project is JS-Interpreter. The documentation covers how to expose various functions to the interpreter, but its scope is otherwise very limited.
As of 2019, vm2 looks like the most popular and most regularly-updated solution to running JavaScript in Node.js. I'm not aware of a front-end solution.
With NISP you'll be able to do sandboxed evaluation.
Though the expression you write is not exactly JavaScript code, instead you'll write S-expressions. It is ideal for simple DSLs that doesn't demand extensive programming.
Suppose you have code to execute:
var sCode = "alert(document)";
Now, suppose you want to execute it in a sandbox:
new Function("window", "with(window){" + sCode + "}")({});
These two lines when executed will fail, because "alert" function is not available from the "sandbox"
And now you want to expose a member of window object with your functionality:
new Function("window", "with(window){" + sCode + "}")({
'alert':function(sString){document.title = sString}
});
Indeed you can add quotes escaping and make other polishing, but I guess the idea is clear.
Where is this user JavaScript code coming from?
There is not much you can do about a user embedding code into your page and then calling it from their browser (see Greasemonkey). It's just something browsers do.
However, if you store the script in a database, then retrieve it and eval() it, then you can clean up the script before it is run.
Examples of code that removes all window. and document. references:
eval(
unsafeUserScript
.replace(/\/\/.+\n|\/\*.*\*\/, '') // Clear all comments
.replace(/\s(window|document)\s*[\;\)\.]/, '') // Removes window. Or window; or window)
)
This tries to prevent the following from being executed (not tested):
window.location = 'http://example.com';
var w = window;
There are a lot of limitations you would have to apply to the unsafe user script. Unfortunately, there isn't any 'sandbox container' available for JavaScript.
I've been working on a simplistic JavaScript sandbox for letting users build applets for my site. Although I still face some challenges with allowing DOM access (parentNode just won't let me keep things secure =/), my approach was just to redefine the window object with some of its useful/harmless members, and then eval() the user code with this redefined window as the default scope.
My "core" code goes like this... (I'm not showing it entirely ;)
function Sandbox(parent){
this.scope = {
window: {
alert: function(str){
alert("Overriden Alert: " + str);
},
prompt: function(message, defaultValue){
return prompt("Overriden Prompt:" + message, defaultValue);
},
document: null,
.
.
.
.
}
};
this.execute = function(codestring){
// Here some code sanitizing, please
with (this.scope) {
with (window) {
eval(codestring);
}
}
};
}
So, I can instantiate a Sandbox and use its execute() function to get code running. Also, all new declared variables within eval'd code will ultimately bound to the execute() scope, so there will not be clashing names or messing with existing code.
Although global objects will still be accessible, those which should remain unknown to the sandboxed code must be defined as proxies in the Sandbox::scope object.
You can wrap the user's code in a function that redefines forbidden objects as parameters -- these would then be undefined when called:
(function (alert) {
alert ("uh oh!"); // User code
}) ();
Of course, clever attackers can get around this by inspecting the JavaScript DOM and finding a non-overridden object that contains a reference to the window.
Another idea is scanning the user's code using a tool like JSLint. Make sure it's set to have no preset variables (or: only variables you want), and then if any globals are set or accessed do not let the user's script be used. Again, it might be vulnerable to walking the DOM -- objects that the user can construct using literals might have implicit references to the window object that could be accessed to escape the sandbox.

Categories

Resources