UNPKG

@nuxeo/nuxeo-ui-elements

Version:
1,514 lines (1,417 loc) 138 kB
/** * @license * Copyright 2013 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Interpreting JavaScript in JavaScript. * @author fraser@google.com (Neil Fraser) */ 'use strict'; import { Parser as acorn } from 'acorn'; /** * Create a new interpreter. * @param {string|!Object} code Raw JavaScript text or AST. * @param {Function=} opt_initFunc Optional initialization function. Used to * define APIs. When called it is passed the interpreter object and the * global scope object. * @constructor */ var Interpreter = function(code, opt_initFunc) { if (typeof code === 'string') { code = acorn.parse(code, Interpreter.PARSE_OPTIONS); } // Get a handle on Acorn's node_t object. this.nodeConstructor = code.constructor; // Clone the root 'Program' node so that the AST may be modified. var ast = new this.nodeConstructor({ options: {} }); for (var prop in code) { ast[prop] = prop === 'body' ? code[prop].slice() : code[prop]; } this.ast = ast; this.initFunc_ = opt_initFunc; this.paused_ = false; this.polyfills_ = []; // Unique identifier for native functions. Used in serialization. this.functionCounter_ = 0; // Map node types to our step function names; a property lookup is faster // than string concatenation with "step" prefix. this.stepFunctions_ = Object.create(null); var stepMatch = /^step([A-Z]\w*)$/; var m; for (var methodName in this) { if (typeof this[methodName] === 'function' && (m = methodName.match(stepMatch))) { this.stepFunctions_[m[1]] = this[methodName].bind(this); } } // Create and initialize the global scope. this.globalScope = this.createScope(this.ast, null); this.globalObject = this.globalScope.object; // Run the polyfills. this.ast = acorn.parse(this.polyfills_.join('\n'), Interpreter.PARSE_OPTIONS); this.polyfills_ = undefined; // Allow polyfill strings to garbage collect. Interpreter.stripLocations_(this.ast, undefined, undefined); var state = new Interpreter.State(this.ast, this.globalScope); state.done = false; this.stateStack = [state]; this.run(); this.value = undefined; // Point at the main program. this.ast = ast; var state = new Interpreter.State(this.ast, this.globalScope); state.done = false; this.stateStack.length = 0; this.stateStack[0] = state; // Preserve publicly properties from being pruned/renamed by JS compilers. // Add others as needed. this['stateStack'] = this.stateStack; }; /** * Completion Value Types. * @enum {number} */ Interpreter.Completion = { NORMAL: 0, BREAK: 1, CONTINUE: 2, RETURN: 3, THROW: 4, }; /** * @const {!Object} Configuration used for all Acorn parsing. */ Interpreter.PARSE_OPTIONS = { ecmaVersion: 5, }; /** * Property descriptor of readonly properties. */ Interpreter.READONLY_DESCRIPTOR = { configurable: true, enumerable: true, writable: false, }; /** * Property descriptor of non-enumerable properties. */ Interpreter.NONENUMERABLE_DESCRIPTOR = { configurable: true, enumerable: false, writable: true, }; /** * Property descriptor of readonly, non-enumerable properties. */ Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR = { configurable: true, enumerable: false, writable: false, }; /** * Property descriptor of variables. */ Interpreter.VARIABLE_DESCRIPTOR = { configurable: false, enumerable: true, writable: true, }; /** * Unique symbol for indicating that a step has encountered an error, has * added it to the stack, and will be thrown within the user's program. * When STEP_ERROR is thrown in the JS-Interpreter, the error can be ignored. */ Interpreter.STEP_ERROR = { STEP_ERROR: true }; /** * Unique symbol for indicating that a reference is a variable on the scope, * not an object property. */ Interpreter.SCOPE_REFERENCE = { SCOPE_REFERENCE: true }; /** * Unique symbol for indicating, when used as the value of the value * parameter in calls to setProperty and friends, that the value * should be taken from the property descriptor instead. */ Interpreter.VALUE_IN_DESCRIPTOR = { VALUE_IN_DESCRIPTOR: true }; /** * Unique symbol for indicating that a RegExp timeout has occurred in a VM. */ Interpreter.REGEXP_TIMEOUT = { REGEXP_TIMEOUT: true }; /** * For cycle detection in array to string and error conversion; * see spec bug github.com/tc39/ecma262/issues/289 * Since this is for atomic actions only, it can be a class property. */ Interpreter.toStringCycles_ = []; /** * Node's vm module, if loaded and required. * @type {Object} */ Interpreter.vm = null; /** * Code for executing regular expressions in a thread. */ Interpreter.WORKER_CODE = [ 'onmessage = function(e) {', 'var result;', 'var data = e.data;', 'switch (data[0]) {', "case 'split':", // ['split', string, separator, limit] 'result = data[1].split(data[2], data[3]);', 'break;', "case 'match':", // ['match', string, regexp] 'result = data[1].match(data[2]);', 'break;', "case 'search':", // ['search', string, regexp] 'result = data[1].search(data[2]);', 'break;', "case 'replace':", // ['replace', string, regexp, newSubstr] 'result = data[1].replace(data[2], data[3]);', 'break;', "case 'exec':", // ['exec', regexp, lastIndex, string] 'var regexp = data[1];', 'regexp.lastIndex = data[2];', 'result = [regexp.exec(data[3]), data[1].lastIndex];', 'break;', 'default:', "throw Error('Unknown RegExp operation: ' + data[0]);", '}', 'postMessage(result);', '};', ]; /** * Is a value a legal integer for an array length? * @param {Interpreter.Value} x Value to check. * @return {number} Zero, or a positive integer if the value can be * converted to such. NaN otherwise. */ Interpreter.legalArrayLength = function(x) { var n = x >>> 0; // Array length must be between 0 and 2^32-1 (inclusive). return n === Number(x) ? n : NaN; }; /** * Is a value a legal integer for an array index? * @param {Interpreter.Value} x Value to check. * @return {number} Zero, or a positive integer if the value can be * converted to such. NaN otherwise. */ Interpreter.legalArrayIndex = function(x) { var n = x >>> 0; // Array index cannot be 2^32-1, otherwise length would be 2^32. // 0xffffffff is 2^32-1. return String(n) === String(x) && n !== 0xffffffff ? n : NaN; }; /** * Remove start and end values from AST, or set start and end values to a * constant value. Used to remove highlighting from polyfills and to set * highlighting in an eval to cover the entire eval expression. * @param {!Object} node AST node. * @param {number=} start Starting character of all nodes, or undefined. * @param {number=} end Ending character of all nodes, or undefined. * @private */ Interpreter.stripLocations_ = function(node, start, end) { if (start) { node['start'] = start; } else { delete node['start']; } if (end) { node['end'] = end; } else { delete node['end']; } for (var name in node) { if (node.hasOwnProperty(name)) { var prop = node[name]; if (prop && typeof prop === 'object') { Interpreter.stripLocations_(prop, start, end); } } } }; /** * Some pathological regular expressions can take geometric time. * Regular expressions are handled in one of three ways: * 0 - throw as invalid. * 1 - execute natively (risk of unresponsive program). * 2 - execute in separate thread (not supported by IE 9). */ Interpreter.prototype['REGEXP_MODE'] = 2; /** * If REGEXP_MODE = 2, the length of time (in ms) to allow a RegExp * thread to execute before terminating it. */ Interpreter.prototype['REGEXP_THREAD_TIMEOUT'] = 1000; /** * Flag indicating that a getter function needs to be called immediately. * @private */ Interpreter.prototype.getterStep_ = false; /** * Flag indicating that a setter function needs to be called immediately. * @private */ Interpreter.prototype.setterStep_ = false; /** * Add more code to the interpreter. * @param {string|!Object} code Raw JavaScript text or AST. */ Interpreter.prototype.appendCode = function(code) { var state = this.stateStack[0]; if (!state || state.node['type'] !== 'Program') { throw Error('Expecting original AST to start with a Program node.'); } if (typeof code === 'string') { code = acorn.parse(code, Interpreter.PARSE_OPTIONS); } if (!code || code['type'] !== 'Program') { throw Error('Expecting new AST to start with a Program node.'); } this.populateScope_(code, state.scope); // Append the new program to the old one. Array.prototype.push.apply(state.node['body'], code['body']); state.done = false; }; /** * Execute one step of the interpreter. * @return {boolean} True if a step was executed, false if no more instructions. */ Interpreter.prototype.step = function() { var stack = this.stateStack; do { var state = stack[stack.length - 1]; if (!state) { return false; } var node = state.node, type = node['type']; if (type === 'Program' && state.done) { return false; } else if (this.paused_) { return true; } try { var nextState = this.stepFunctions_[type](stack, state, node); } catch (e) { // Eat any step errors. They have been thrown on the stack. if (e !== Interpreter.STEP_ERROR) { // Uh oh. This is a real error in the JS-Interpreter. Rethrow. throw e; } } if (nextState) { stack.push(nextState); } if (this.getterStep_) { // Getter from this step was not handled. throw Error('Getter not supported in this context'); } if (this.setterStep_) { // Setter from this step was not handled. throw Error('Setter not supported in this context'); } // This may be polyfill code. Keep executing until we arrive at user code. } while (!node['end']); return true; }; /** * Execute the interpreter to program completion. Vulnerable to infinite loops. * @return {boolean} True if a execution is asynchronously blocked, * false if no more instructions. */ Interpreter.prototype.run = function() { while (!this.paused_ && this.step()) {} return this.paused_; }; /** * Initialize the global object with buitin properties and functions. * @param {!Interpreter.Object} globalObject Global object. */ Interpreter.prototype.initGlobal = function(globalObject) { // Initialize uneditable global properties. this.setProperty(globalObject, 'NaN', NaN, Interpreter.READONLY_DESCRIPTOR); this.setProperty(globalObject, 'Infinity', Infinity, Interpreter.READONLY_DESCRIPTOR); this.setProperty(globalObject, 'undefined', undefined, Interpreter.READONLY_DESCRIPTOR); this.setProperty(globalObject, 'window', globalObject, Interpreter.READONLY_DESCRIPTOR); this.setProperty(globalObject, 'this', globalObject); // WEBUI-60: Editable this.setProperty(globalObject, 'self', globalObject); // Editable. // Create the objects which will become Object.prototype and // Function.prototype, which are needed to bootstrap everything else. this.OBJECT_PROTO = new Interpreter.Object(null); this.FUNCTION_PROTO = new Interpreter.Object(this.OBJECT_PROTO); // Initialize global objects. this.initFunction(globalObject); this.initObject(globalObject); // Unable to set globalObject's parent prior (OBJECT did not exist). // Note that in a browser this would be `Window`, whereas in Node.js it would // be `Object`. This interpreter is closer to Node in that it has no DOM. globalObject.proto = this.OBJECT_PROTO; this.setProperty(globalObject, 'constructor', this.OBJECT, Interpreter.NONENUMERABLE_DESCRIPTOR); this.initArray(globalObject); this.initString(globalObject); this.initBoolean(globalObject); this.initNumber(globalObject); this.initDate(globalObject); this.initRegExp(globalObject); this.initError(globalObject); this.initMath(globalObject); this.initJSON(globalObject); // Initialize global functions. var thisInterpreter = this; var func = this.createNativeFunction(function(x) { throw EvalError("Can't happen"); }, false); func.eval = true; this.setProperty(globalObject, 'eval', func); this.setProperty(globalObject, 'parseInt', this.createNativeFunction(parseInt, false)); this.setProperty(globalObject, 'parseFloat', this.createNativeFunction(parseFloat, false)); this.setProperty(globalObject, 'isNaN', this.createNativeFunction(isNaN, false)); this.setProperty(globalObject, 'isFinite', this.createNativeFunction(isFinite, false)); var strFunctions = [ [escape, 'escape'], [unescape, 'unescape'], [decodeURI, 'decodeURI'], [decodeURIComponent, 'decodeURIComponent'], [encodeURI, 'encodeURI'], [encodeURIComponent, 'encodeURIComponent'], ]; for (var i = 0; i < strFunctions.length; i++) { var wrapper = (function(nativeFunc) { return function(str) { try { return nativeFunc(str); } catch (e) { // decodeURI('%xy') will throw an error. Catch and rethrow. thisInterpreter.throwException(thisInterpreter.URI_ERROR, e.message); } }; })(strFunctions[i][0]); this.setProperty( globalObject, strFunctions[i][1], this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR, ); } // Preserve publicly properties from being pruned/renamed by JS compilers. // Add others as needed. this['OBJECT'] = this.OBJECT; this['OBJECT_PROTO'] = this.OBJECT_PROTO; this['FUNCTION'] = this.FUNCTION; this['FUNCTION_PROTO'] = this.FUNCTION_PROTO; this['ARRAY'] = this.ARRAY; this['ARRAY_PROTO'] = this.ARRAY_PROTO; this['REGEXP'] = this.REGEXP; this['REGEXP_PROTO'] = this.REGEXP_PROTO; this['DATE'] = this.DATE; this['DATE_PROTO'] = this.DATE_PROTO; // Run any user-provided initialization. if (this.initFunc_) { this.initFunc_(this, globalObject); } }; /** * Initialize the Function class. * @param {!Interpreter.Object} globalObject Global object. */ Interpreter.prototype.initFunction = function(globalObject) { var thisInterpreter = this; var wrapper; var identifierRegexp = /^[A-Za-z_$][\w$]*$/; // Function constructor. wrapper = function(var_args) { if (arguments.length) { var code = String(arguments[arguments.length - 1]); } else { var code = ''; } var argsStr = Array.prototype.slice .call(arguments, 0, -1) .join(',') .trim(); if (argsStr) { var args = argsStr.split(/\s*,\s*/); for (var i = 0; i < args.length; i++) { var name = args[i]; if (!identifierRegexp.test(name)) { thisInterpreter.throwException(thisInterpreter.SYNTAX_ERROR, 'Invalid function argument: ' + name); } } argsStr = args.join(', '); } // Acorn needs to parse code in the context of a function or else `return` // statements will be syntax errors. try { var ast = acorn.parse('(function(' + argsStr + ') {' + code + '})', Interpreter.PARSE_OPTIONS); } catch (e) { // Acorn threw a SyntaxError. Rethrow as a trappable error. thisInterpreter.throwException(thisInterpreter.SYNTAX_ERROR, 'Invalid code: ' + e.message); } if (ast['body'].length !== 1) { // Function('a', 'return a + 6;}; {alert(1);'); thisInterpreter.throwException(thisInterpreter.SYNTAX_ERROR, 'Invalid code in function body.'); } var node = ast['body'][0]['expression']; // Note that if this constructor is called as `new Function()` the function // object created by stepCallExpression and assigned to `this` is discarded. // Interestingly, the scope for constructed functions is the global scope, // even if they were constructed in some other scope. return thisInterpreter.createFunction(node, thisInterpreter.globalScope); }; this.FUNCTION = this.createNativeFunction(wrapper, true); this.setProperty(globalObject, 'Function', this.FUNCTION); // Throw away the created prototype and use the root prototype. this.setProperty(this.FUNCTION, 'prototype', this.FUNCTION_PROTO, Interpreter.NONENUMERABLE_DESCRIPTOR); // Configure Function.prototype. this.setProperty(this.FUNCTION_PROTO, 'constructor', this.FUNCTION, Interpreter.NONENUMERABLE_DESCRIPTOR); this.FUNCTION_PROTO.nativeFunc = function() {}; this.FUNCTION_PROTO.nativeFunc.id = this.functionCounter_++; this.setProperty(this.FUNCTION_PROTO, 'length', 0, Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); var boxThis = function(value) { // In non-strict mode `this` must be an object. if (!(value instanceof Interpreter.Object) && !thisInterpreter.getScope().strict) { if (value === undefined || value === null) { // `Undefined` and `null` are changed to the global object. value = thisInterpreter.globalObject; } else { // Primitives must be boxed in non-strict mode. var box = thisInterpreter.createObjectProto(thisInterpreter.getPrototype(value)); box.data = value; value = box; } } return value; }; wrapper = function(thisArg, args) { var state = thisInterpreter.stateStack[thisInterpreter.stateStack.length - 1]; // Rewrite the current CallExpression state to apply a different function. state.func_ = this; // Assign the `this` object. state.funcThis_ = boxThis(thisArg); // Bind any provided arguments. state.arguments_ = []; if (args !== null && args !== undefined) { if (args instanceof Interpreter.Object) { state.arguments_ = thisInterpreter.arrayPseudoToNative(args); } else { thisInterpreter.throwException(thisInterpreter.TYPE_ERROR, 'CreateListFromArrayLike called on non-object'); } } state.doneExec_ = false; }; this.setNativeFunctionPrototype(this.FUNCTION, 'apply', wrapper); wrapper = function(thisArg /*, var_args */) { var state = thisInterpreter.stateStack[thisInterpreter.stateStack.length - 1]; // Rewrite the current CallExpression state to call a different function. state.func_ = this; // Assign the `this` object. state.funcThis_ = boxThis(thisArg); // Bind any provided arguments. state.arguments_ = []; for (var i = 1; i < arguments.length; i++) { state.arguments_.push(arguments[i]); } state.doneExec_ = false; }; this.setNativeFunctionPrototype(this.FUNCTION, 'call', wrapper); this.polyfills_.push( // Polyfill copied from: // developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_objects/Function/bind "Object.defineProperty(Function.prototype, 'bind',", '{configurable: true, writable: true, value:', 'function(oThis) {', "if (typeof this !== 'function') {", "throw TypeError('What is trying to be bound is not callable');", '}', 'var aArgs = Array.prototype.slice.call(arguments, 1),', 'fToBind = this,', 'fNOP = function() {},', 'fBound = function() {', 'return fToBind.apply(this instanceof fNOP', '? this', ': oThis,', 'aArgs.concat(Array.prototype.slice.call(arguments)));', '};', 'if (this.prototype) {', 'fNOP.prototype = this.prototype;', '}', 'fBound.prototype = new fNOP();', 'return fBound;', '}', '});', '', ); // Function has no parent to inherit from, so it needs its own mandatory // toString and valueOf functions. wrapper = function() { return String(this); }; this.setNativeFunctionPrototype(this.FUNCTION, 'toString', wrapper); this.setProperty( this.FUNCTION, 'toString', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR, ); wrapper = function() { return this.valueOf(); }; this.setNativeFunctionPrototype(this.FUNCTION, 'valueOf', wrapper); this.setProperty( this.FUNCTION, 'valueOf', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR, ); }; /** * Initialize the Object class. * @param {!Interpreter.Object} globalObject Global object. */ Interpreter.prototype.initObject = function(globalObject) { var thisInterpreter = this; var wrapper; // Object constructor. wrapper = function(value) { if (value === undefined || value === null) { // Create a new object. if (thisInterpreter.calledWithNew()) { // Called as `new Object()`. return this; } else { // Called as `Object()`. return thisInterpreter.createObjectProto(thisInterpreter.OBJECT_PROTO); } } if (!(value instanceof Interpreter.Object)) { // Wrap the value as an object. var box = thisInterpreter.createObjectProto(thisInterpreter.getPrototype(value)); box.data = value; return box; } // Return the provided object. return value; }; this.OBJECT = this.createNativeFunction(wrapper, true); // Throw away the created prototype and use the root prototype. this.setProperty(this.OBJECT, 'prototype', this.OBJECT_PROTO, Interpreter.NONENUMERABLE_DESCRIPTOR); this.setProperty(this.OBJECT_PROTO, 'constructor', this.OBJECT, Interpreter.NONENUMERABLE_DESCRIPTOR); this.setProperty(globalObject, 'Object', this.OBJECT); /** * Checks if the provided value is null or undefined. * If so, then throw an error in the call stack. * @param {Interpreter.Value} value Value to check. */ var throwIfNullUndefined = function(value) { if (value === undefined || value === null) { thisInterpreter.throwException(thisInterpreter.TYPE_ERROR, "Cannot convert '" + value + "' to object"); } }; // Static methods on Object. wrapper = function(obj) { throwIfNullUndefined(obj); var props = obj instanceof Interpreter.Object ? obj.properties : obj; return thisInterpreter.arrayNativeToPseudo(Object.getOwnPropertyNames(props)); }; this.setProperty( this.OBJECT, 'getOwnPropertyNames', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR, ); wrapper = function(obj) { throwIfNullUndefined(obj); if (obj instanceof Interpreter.Object) { obj = obj.properties; } return thisInterpreter.arrayNativeToPseudo(Object.keys(obj)); }; this.setProperty( this.OBJECT, 'keys', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR, ); wrapper = function(proto) { // Support for the second argument is the responsibility of a polyfill. if (proto === null) { return thisInterpreter.createObjectProto(null); } if (!(proto instanceof Interpreter.Object)) { thisInterpreter.throwException(thisInterpreter.TYPE_ERROR, 'Object prototype may only be an Object or null'); } return thisInterpreter.createObjectProto(proto); }; this.setProperty( this.OBJECT, 'create', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR, ); // Add a polyfill to handle create's second argument. this.polyfills_.push( '(function() {', 'var create_ = Object.create;', 'Object.create = function(proto, props) {', 'var obj = create_(proto);', 'props && Object.defineProperties(obj, props);', 'return obj;', '};', '})();', '', ); wrapper = function(obj, prop, descriptor) { prop = String(prop); if (!(obj instanceof Interpreter.Object)) { thisInterpreter.throwException(thisInterpreter.TYPE_ERROR, 'Object.defineProperty called on non-object'); } if (!(descriptor instanceof Interpreter.Object)) { thisInterpreter.throwException(thisInterpreter.TYPE_ERROR, 'Property description must be an object'); } if (!obj.properties[prop] && obj.preventExtensions) { thisInterpreter.throwException( thisInterpreter.TYPE_ERROR, "Can't define property '" + prop + "', object is not extensible", ); } // The polyfill guarantees no inheritance and no getter functions. // Therefore the descriptor properties map is the native object needed. thisInterpreter.setProperty(obj, prop, Interpreter.VALUE_IN_DESCRIPTOR, descriptor.properties); return obj; }; this.setProperty( this.OBJECT, 'defineProperty', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR, ); this.polyfills_.push( // Flatten the descriptor to remove any inheritance or getter functions. '(function() {', 'var defineProperty_ = Object.defineProperty;', 'Object.defineProperty = function(obj, prop, d1) {', 'var d2 = {};', "if ('configurable' in d1) d2.configurable = d1.configurable;", "if ('enumerable' in d1) d2.enumerable = d1.enumerable;", "if ('writable' in d1) d2.writable = d1.writable;", "if ('value' in d1) d2.value = d1.value;", "if ('get' in d1) d2.get = d1.get;", "if ('set' in d1) d2.set = d1.set;", 'return defineProperty_(obj, prop, d2);', '};', '})();', "Object.defineProperty(Object, 'defineProperties',", '{configurable: true, writable: true, value:', 'function(obj, props) {', 'var keys = Object.keys(props);', 'for (var i = 0; i < keys.length; i++) {', 'Object.defineProperty(obj, keys[i], props[keys[i]]);', '}', 'return obj;', '}', '});', '', ); wrapper = function(obj, prop) { if (!(obj instanceof Interpreter.Object)) { thisInterpreter.throwException( thisInterpreter.TYPE_ERROR, 'Object.getOwnPropertyDescriptor called on non-object', ); } prop = String(prop); if (!(prop in obj.properties)) { return undefined; } var descriptor = Object.getOwnPropertyDescriptor(obj.properties, prop); var getter = obj.getter[prop]; var setter = obj.setter[prop]; var pseudoDescriptor = thisInterpreter.createObjectProto(thisInterpreter.OBJECT_PROTO); if (getter || setter) { thisInterpreter.setProperty(pseudoDescriptor, 'get', getter); thisInterpreter.setProperty(pseudoDescriptor, 'set', setter); } else { thisInterpreter.setProperty(pseudoDescriptor, 'value', descriptor.value); thisInterpreter.setProperty(pseudoDescriptor, 'writable', descriptor.writable); } thisInterpreter.setProperty(pseudoDescriptor, 'configurable', descriptor.configurable); thisInterpreter.setProperty(pseudoDescriptor, 'enumerable', descriptor.enumerable); return pseudoDescriptor; }; this.setProperty( this.OBJECT, 'getOwnPropertyDescriptor', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR, ); wrapper = function(obj) { throwIfNullUndefined(obj); return thisInterpreter.getPrototype(obj); }; this.setProperty( this.OBJECT, 'getPrototypeOf', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR, ); wrapper = function(obj) { return Boolean(obj) && !obj.preventExtensions; }; this.setProperty( this.OBJECT, 'isExtensible', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR, ); wrapper = function(obj) { if (obj instanceof Interpreter.Object) { obj.preventExtensions = true; } return obj; }; this.setProperty( this.OBJECT, 'preventExtensions', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR, ); // Instance methods on Object. this.setNativeFunctionPrototype(this.OBJECT, 'toString', Interpreter.Object.prototype.toString); this.setNativeFunctionPrototype(this.OBJECT, 'toLocaleString', Interpreter.Object.prototype.toString); this.setNativeFunctionPrototype(this.OBJECT, 'valueOf', Interpreter.Object.prototype.valueOf); wrapper = function(prop) { throwIfNullUndefined(this); if (this instanceof Interpreter.Object) { return String(prop) in this.properties; } // Primitive. return this.hasOwnProperty(prop); }; this.setNativeFunctionPrototype(this.OBJECT, 'hasOwnProperty', wrapper); wrapper = function(prop) { throwIfNullUndefined(this); if (this instanceof Interpreter.Object) { return Object.prototype.propertyIsEnumerable.call(this.properties, prop); } // Primitive. return this.propertyIsEnumerable(prop); }; this.setNativeFunctionPrototype(this.OBJECT, 'propertyIsEnumerable', wrapper); wrapper = function(obj) { while (true) { // Note, circular loops shouldn't be possible. obj = thisInterpreter.getPrototype(obj); if (!obj) { // No parent; reached the top. return false; } if (obj === this) { return true; } } }; this.setNativeFunctionPrototype(this.OBJECT, 'isPrototypeOf', wrapper); }; /** * Initialize the Array class. * @param {!Interpreter.Object} globalObject Global object. */ Interpreter.prototype.initArray = function(globalObject) { var thisInterpreter = this; var wrapper; // Array constructor. wrapper = function(var_args) { if (thisInterpreter.calledWithNew()) { // Called as `new Array()`. var newArray = this; } else { // Called as `Array()`. var newArray = thisInterpreter.createArray(); } var first = arguments[0]; if (arguments.length === 1 && typeof first === 'number') { if (isNaN(Interpreter.legalArrayLength(first))) { thisInterpreter.throwException(thisInterpreter.RANGE_ERROR, 'Invalid array length'); } newArray.properties.length = first; } else { for (var i = 0; i < arguments.length; i++) { newArray.properties[i] = arguments[i]; } newArray.properties.length = i; } return newArray; }; this.ARRAY = this.createNativeFunction(wrapper, true); this.ARRAY_PROTO = this.ARRAY.properties['prototype']; this.setProperty(globalObject, 'Array', this.ARRAY); // Static methods on Array. wrapper = function(obj) { return obj && obj.class === 'Array'; }; this.setProperty( this.ARRAY, 'isArray', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR, ); // Instance methods on Array. this.setProperty(this.ARRAY_PROTO, 'length', 0, { configurable: false, enumerable: false, writable: true }); this.ARRAY_PROTO.class = 'Array'; wrapper = function() { return Array.prototype.pop.call(this.properties); }; this.setNativeFunctionPrototype(this.ARRAY, 'pop', wrapper); wrapper = function(var_args) { return Array.prototype.push.apply(this.properties, arguments); }; this.setNativeFunctionPrototype(this.ARRAY, 'push', wrapper); wrapper = function() { return Array.prototype.shift.call(this.properties); }; this.setNativeFunctionPrototype(this.ARRAY, 'shift', wrapper); wrapper = function(var_args) { return Array.prototype.unshift.apply(this.properties, arguments); }; this.setNativeFunctionPrototype(this.ARRAY, 'unshift', wrapper); wrapper = function() { Array.prototype.reverse.call(this.properties); return this; }; this.setNativeFunctionPrototype(this.ARRAY, 'reverse', wrapper); wrapper = function(index, howmany /*, var_args*/) { var list = Array.prototype.splice.apply(this.properties, arguments); return thisInterpreter.arrayNativeToPseudo(list); }; this.setNativeFunctionPrototype(this.ARRAY, 'splice', wrapper); wrapper = function(opt_begin, opt_end) { var list = Array.prototype.slice.call(this.properties, opt_begin, opt_end); return thisInterpreter.arrayNativeToPseudo(list); }; this.setNativeFunctionPrototype(this.ARRAY, 'slice', wrapper); wrapper = function(opt_separator) { return Array.prototype.join.call(this.properties, opt_separator); }; this.setNativeFunctionPrototype(this.ARRAY, 'join', wrapper); wrapper = function(var_args) { var list = []; var length = 0; // Start by copying the current array. var iLength = thisInterpreter.getProperty(this, 'length'); for (var i = 0; i < iLength; i++) { if (thisInterpreter.hasProperty(this, i)) { var element = thisInterpreter.getProperty(this, i); list[length] = element; } length++; } // Loop through all arguments and copy them in. for (var i = 0; i < arguments.length; i++) { var value = arguments[i]; if (thisInterpreter.isa(value, thisInterpreter.ARRAY)) { var jLength = thisInterpreter.getProperty(value, 'length'); for (var j = 0; j < jLength; j++) { if (thisInterpreter.hasProperty(value, j)) { list[length] = thisInterpreter.getProperty(value, j); } length++; } } else { list[length] = value; } } return thisInterpreter.arrayNativeToPseudo(list); }; this.setNativeFunctionPrototype(this.ARRAY, 'concat', wrapper); wrapper = function(searchElement, opt_fromIndex) { return Array.prototype.indexOf.apply(this.properties, arguments); }; this.setNativeFunctionPrototype(this.ARRAY, 'indexOf', wrapper); wrapper = function(searchElement, opt_fromIndex) { return Array.prototype.lastIndexOf.apply(this.properties, arguments); }; this.setNativeFunctionPrototype(this.ARRAY, 'lastIndexOf', wrapper); wrapper = function() { Array.prototype.sort.call(this.properties); return this; }; this.setNativeFunctionPrototype(this.ARRAY, 'sort', wrapper); // WEBUI-60: make Array.includes() available wrapper = function(searchElement, opt_fromIndex) { return Array.prototype.includes.call(this.properties, arguments); }; this.setNativeFunctionPrototype(this.ARRAY, 'includes', wrapper); this.polyfills_.push( // Polyfill copied from: // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/every "Object.defineProperty(Array.prototype, 'every',", '{configurable: true, writable: true, value:', 'function(callbackfn, thisArg) {', "if (!this || typeof callbackfn !== 'function') throw TypeError();", 'var T, k;', 'var O = Object(this);', 'var len = O.length >>> 0;', 'if (arguments.length > 1) T = thisArg;', 'k = 0;', 'while (k < len) {', 'if (k in O && !callbackfn.call(T, O[k], k, O)) return false;', 'k++;', '}', 'return true;', '}', '});', // Polyfill copied from: // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/filter "Object.defineProperty(Array.prototype, 'filter',", '{configurable: true, writable: true, value:', 'function(fun/*, thisArg*/) {', "if (this === void 0 || this === null || typeof fun !== 'function') throw TypeError();", 'var t = Object(this);', 'var len = t.length >>> 0;', 'var res = [];', 'var thisArg = arguments.length >= 2 ? arguments[1] : void 0;', 'for (var i = 0; i < len; i++) {', 'if (i in t) {', 'var val = t[i];', 'if (fun.call(thisArg, val, i, t)) res.push(val);', '}', '}', 'return res;', '}', '});', // Polyfill copied from: // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach "Object.defineProperty(Array.prototype, 'forEach',", '{configurable: true, writable: true, value:', 'function(callback, thisArg) {', "if (!this || typeof callback !== 'function') throw TypeError();", 'var T, k;', 'var O = Object(this);', 'var len = O.length >>> 0;', 'if (arguments.length > 1) T = thisArg;', 'k = 0;', 'while (k < len) {', 'if (k in O) callback.call(T, O[k], k, O);', 'k++;', '}', '}', '});', // Polyfill copied from: // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/map "Object.defineProperty(Array.prototype, 'map',", '{configurable: true, writable: true, value:', 'function(callback, thisArg) {', "if (!this || typeof callback !== 'function') new TypeError;", 'var T, A, k;', 'var O = Object(this);', 'var len = O.length >>> 0;', 'if (arguments.length > 1) T = thisArg;', 'A = new Array(len);', 'k = 0;', 'while (k < len) {', 'if (k in O) A[k] = callback.call(T, O[k], k, O);', 'k++;', '}', 'return A;', '}', '});', // Polyfill copied from: // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce "Object.defineProperty(Array.prototype, 'reduce',", '{configurable: true, writable: true, value:', 'function(callback /*, initialValue*/) {', "if (!this || typeof callback !== 'function') throw TypeError();", 'var t = Object(this), len = t.length >>> 0, k = 0, value;', 'if (arguments.length === 2) {', 'value = arguments[1];', '} else {', 'while (k < len && !(k in t)) k++;', 'if (k >= len) {', "throw TypeError('Reduce of empty array with no initial value');", '}', 'value = t[k++];', '}', 'for (; k < len; k++) {', 'if (k in t) value = callback(value, t[k], k, t);', '}', 'return value;', '}', '});', // Polyfill copied from: // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/ReduceRight "Object.defineProperty(Array.prototype, 'reduceRight',", '{configurable: true, writable: true, value:', 'function(callback /*, initialValue*/) {', "if (null === this || 'undefined' === typeof this || 'function' !== typeof callback) throw TypeError();", 'var t = Object(this), len = t.length >>> 0, k = len - 1, value;', 'if (arguments.length >= 2) {', 'value = arguments[1];', '} else {', 'while (k >= 0 && !(k in t)) k--;', 'if (k < 0) {', "throw TypeError('Reduce of empty array with no initial value');", '}', 'value = t[k--];', '}', 'for (; k >= 0; k--) {', 'if (k in t) value = callback(value, t[k], k, t);', '}', 'return value;', '}', '});', // Polyfill copied from: // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/some "Object.defineProperty(Array.prototype, 'some',", '{configurable: true, writable: true, value:', 'function(fun/*, thisArg*/) {', "if (!this || typeof fun !== 'function') throw TypeError();", 'var t = Object(this);', 'var len = t.length >>> 0;', 'var thisArg = arguments.length >= 2 ? arguments[1] : void 0;', 'for (var i = 0; i < len; i++) {', 'if (i in t && fun.call(thisArg, t[i], i, t)) {', 'return true;', '}', '}', 'return false;', '}', '});', '(function() {', 'var sort_ = Array.prototype.sort;', 'Array.prototype.sort = function(opt_comp) {', // Fast native sort. "if (typeof opt_comp !== 'function') {", 'return sort_.call(this);', '}', // Slow bubble sort. 'for (var i = 0; i < this.length; i++) {', 'var changes = 0;', 'for (var j = 0; j < this.length - i - 1; j++) {', 'if (opt_comp(this[j], this[j + 1]) > 0) {', 'var swap = this[j];', 'this[j] = this[j + 1];', 'this[j + 1] = swap;', 'changes++;', '}', '}', 'if (!changes) break;', '}', 'return this;', '};', '})();', "Object.defineProperty(Array.prototype, 'toLocaleString',", '{configurable: true, writable: true, value:', 'function() {', 'var out = [];', 'for (var i = 0; i < this.length; i++) {', "out[i] = (this[i] === null || this[i] === undefined) ? '' : this[i].toLocaleString();", '}', "return out.join(',');", '}', '});', '', ); }; /** * Initialize the String class. * @param {!Interpreter.Object} globalObject Global object. */ Interpreter.prototype.initString = function(globalObject) { var thisInterpreter = this; var wrapper; // String constructor. wrapper = function(value) { value = arguments.length ? String(value) : ''; if (thisInterpreter.calledWithNew()) { // Called as `new String()`. this.data = value; return this; } else { // Called as `String()`. return value; } }; this.STRING = this.createNativeFunction(wrapper, true); this.setProperty(globalObject, 'String', this.STRING); // Static methods on String. this.setProperty( this.STRING, 'fromCharCode', this.createNativeFunction(String.fromCharCode, false), Interpreter.NONENUMERABLE_DESCRIPTOR, ); // Instance methods on String. // Methods with exclusively primitive arguments. var functions = [ 'charAt', 'charCodeAt', 'concat', 'indexOf', 'lastIndexOf', 'slice', 'substr', 'substring', 'toLocaleLowerCase', 'toLocaleUpperCase', 'toLowerCase', 'toUpperCase', 'trim', ]; for (var i = 0; i < functions.length; i++) { this.setNativeFunctionPrototype(this.STRING, functions[i], String.prototype[functions[i]]); } wrapper = function(compareString, locales, options) { locales = locales ? thisInterpreter.pseudoToNative(locales) : undefined; options = options ? thisInterpreter.pseudoToNative(options) : undefined; return String(this).localeCompare(compareString, locales, options); }; this.setNativeFunctionPrototype(this.STRING, 'localeCompare', wrapper); wrapper = function(separator, limit, callback) { var string = String(this); limit = limit ? Number(limit) : undefined; // Example of catastrophic split RegExp: // 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaac'.split(/^(a+)+b/) if (thisInterpreter.isa(separator, thisInterpreter.REGEXP)) { separator = separator.data; thisInterpreter.maybeThrowRegExp(separator, callback); if (thisInterpreter['REGEXP_MODE'] === 2) { if (Interpreter.vm) { // Run split in vm. var sandbox = { string: string, separator: separator, limit: limit, }; var code = 'string.split(separator, limit)'; var jsList = thisInterpreter.vmCall(code, sandbox, separator, callback); if (jsList !== Interpreter.REGEXP_TIMEOUT) { callback(thisInterpreter.arrayNativeToPseudo(jsList)); } } else { // Run split in separate thread. var splitWorker = thisInterpreter.createWorker(); var pid = thisInterpreter.regExpTimeout(separator, splitWorker, callback); splitWorker.onmessage = function(e) { clearTimeout(pid); callback(thisInterpreter.arrayNativeToPseudo(e.data)); }; splitWorker.postMessage(['split', string, separator, limit]); } return; } } // Run split natively. var jsList = string.split(separator, limit); callback(thisInterpreter.arrayNativeToPseudo(jsList)); }; this.setAsyncFunctionPrototype(this.STRING, 'split', wrapper); wrapper = function(regexp, callback) { var string = String(this); if (thisInterpreter.isa(regexp, thisInterpreter.REGEXP)) { regexp = regexp.data; } else { regexp = new RegExp(regexp); } // Example of catastrophic match RegExp: // 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaac'.match(/^(a+)+b/) thisInterpreter.maybeThrowRegExp(regexp, callback); if (thisInterpreter['REGEXP_MODE'] === 2) { if (Interpreter.vm) { // Run match in vm. var sandbox = { string: string, regexp: regexp, }; var code = 'string.match(regexp)'; var m = thisInterpreter.vmCall(code, sandbox, regexp, callback); if (m !== Interpreter.REGEXP_TIMEOUT) { callback(m && thisInterpreter.arrayNativeToPseudo(m)); } } else { // Run match in separate thread. var matchWorker = thisInterpreter.createWorker(); var pid = thisInterpreter.regExpTimeout(regexp, matchWorker, callback); matchWorker.onmessage = function(e) { clearTimeout(pid); callback(e.data && thisInterpreter.arrayNativeToPseudo(e.data)); }; matchWorker.postMessage(['match', string, regexp]); } return; } // Run match natively. var m = string.match(regexp); callback(m && thisInterpreter.arrayNativeToPseudo(m)); }; this.setAsyncFunctionPrototype(this.STRING, 'match', wrapper); wrapper = function(regexp, callback) { var string = String(this); if (thisInterpreter.isa(regexp, thisInterpreter.REGEXP)) { regexp = regexp.data; } else { regexp = new RegExp(regexp); } // Example of catastrophic search RegExp: // 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaac'.search(/^(a+)+b/) thisInterpreter.maybeThrowRegExp(regexp, callback); if (thisInterpreter['REGEXP_MODE'] === 2) { if (Interpreter.vm) { // Run search in vm. var sandbox = { string: string, regexp: regexp, }; var code = 'string.search(regexp)'; var n = thisInterpreter.vmCall(code, sandbox, regexp, callback); if (n !== Interpreter.REGEXP_TIMEOUT) { callback(n); } } else { // Run search in separate thread. var searchWorker = thisInterpreter.createWorker(); var pid = thisInterpreter.regExpTimeout(regexp, searchWorker, callback); searchWorker.onmessage = function(e) { clearTimeout(pid); callback(e.data); }; searchWorker.postMessage(['search', string, regexp]); } return; } // Run search natively. callback(string.search(regexp)); }; this.setAsyncFunctionPrototype(this.STRING, 'search', wrapper); wrapper = function(substr, newSubstr, callback) { // Support for function replacements is the responsibility of a polyfill. var string = String(this); newSubstr = String(newSubstr); // Example of catastrophic replace RegExp: // 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaac'.replace(/^(a+)+b/, '') if (thisInterpreter.isa(substr, thisInterpreter.REGEXP)) { substr = substr.data; thisInterpreter.maybeThrowRegExp(substr, callback); if (thisInterpreter['REGEXP_MODE'] === 2) { if (Interpreter.vm) { // Run replace in vm. var sandbox = { string: string, substr: substr, newSubstr: newSubstr, }; var code = 'string.replace(substr, newSubstr)'; var str = thisInterpreter.vmCall(code, sandbox, substr, callback); if (str !== Interpreter.REGEXP_TIMEOUT) { callback(str); } } else { // Run replace in separate thread. var replaceWorker = thisInterpreter.createWorker(); var pid = thisInterpreter.regExpTimeout(substr, replaceWorker, callback); replaceWorker.onmessage = function(e) { clearTimeout(pid); callback(e.data); }; replaceWorker.postMessage(['replace', string, substr, newSubstr]); } return; } } // Run replace natively. callback(string.replace(substr, newSubstr)); }; this.setAsyncFunctionPrototype(this.STRING, 'replace', wrapper); // Add a polyfill to handle replace's second argument being a function. this.polyfills_.push( '(function() {', 'var replace_ = String.prototype.replace;', 'String.prototype.replace = function(substr, newSubstr) {', "if (typeof newSubstr !== 'function') {", // string.replace(string|regexp, string) 'return replace_.call(this, substr, newSubstr);', '}', 'var str = this;', 'if (substr instanceof RegExp) {', // string.replace(regexp, function) 'var subs = [];', 'var m = substr.exec(str);', 'while (m) {', 'm.push(m.index, str);', 'var inject = newSubstr.apply(null, m);', 'subs.push([m.index, m[0].length, inject]);', 'm = substr.global ? substr.exec(str) : null;', '}', 'for (var i = subs.length - 1; i >= 0; i--) {', 'str = str.substring(0, subs[i][0]) + subs[i][2] + ' + 'str.substring(subs[i][0] + subs[i][1]);', '}', '} else {', // string.replace(string, function) 'var i = str.indexOf(substr);', 'if (i !== -1) {', 'var inject = newSubstr(str.substr(i, substr.length), i, str);', 'str = str.substring(0, i) + inject + ' + 'str.substring(i + substr.length);', '}', '}', 'return str;', '};', '})();', '', ); }; /** * Initialize the Boolean class. * @param {!Interpreter.Object} globalObject Global object. */ Interpreter.prototype.initBoolean = function(globalObject) { var thisInterpreter = this; var wrapper; // Boolean constructor. wrapper = function(value) { value = Boolean(value); if (thisInterpreter.calledWithNew()) { // Called as `new Boolean()`. this.data = value; return this; } else { // Called as `Boolean()`. return value; } }; this.BOOLEAN = this.createNativeFunction(wrapper, true); this.setProperty(globalObject, 'Boolean', this.BOOLEAN); }; /** * Initialize the Number class. * @param {!Interpreter.Object} globalObject Global object. */ Interpreter.prototype.initNumber = function(globalObject) { var thisInterpreter = this; var wrapper; // Number constructor. wrapper = function(value) { value = arguments.length ? Number(value) : 0; if (thisInterpreter.calledWithNew()) { // Called as `new Number()`. this.data = value; return this; } else { // Called as `Number()`. return value; } }; this.NUMBER = this.createNativeFunction(wrapper, true); this.setProperty(globalObject, 'Number', this.NUMBER); var numConsts = ['MAX_VALUE', 'MIN_VALUE', 'NaN', 'NEGATIVE_INFINITY', 'POSITIVE_INFINITY']; for (var i = 0; i < numConsts.length; i++) { this.setProperty(this.NUMBER, numConsts[i], Number[numConsts[i]], Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); } // Instance methods on Number. wrapper = function(fractionDigits) { try { return Number(this).toExponential(fractionDigits); } catch (e) { // Throws if fractionDigits isn't within 0-20.