Cscott has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/207816

Change subject: Ensure that embedded Maps and Sets are properly deep-frozen.
......................................................................

Ensure that embedded Maps and Sets are properly deep-frozen.

Object.freeze() doesn't actually have any effect on Map and Set, since
their mutable data is held in a hidden internal field.  Create new
ReadOnlyMap and ReadOnlySet implementations which use a private
scoped variable to ensure that the mutable Map/Set backing it is
safely protected.

Tweak deepFreeze to recurse correctly through included maps and sets,
freezing the keys/values as appropriate.

Since the frozen map/set is a different object than the original map/set,
change deepFreeze to return the frozen object.

Change-Id: Ia16738e88aa3ee9c46fd71da50ce66b8917a07d3
---
M lib/ext.core.Sanitizer.js
M lib/jsutils.js
M lib/mediawiki.ParsoidConfig.js
M lib/mediawiki.WikiConfig.js
M lib/mediawiki.wikitext.constants.js
M lib/wts.SerializerState.js
6 files changed, 105 insertions(+), 18 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/services/parsoid 
refs/changes/16/207816/1

diff --git a/lib/ext.core.Sanitizer.js b/lib/ext.core.Sanitizer.js
index 62b52ec..c1df8b2 100644
--- a/lib/ext.core.Sanitizer.js
+++ b/lib/ext.core.Sanitizer.js
@@ -576,7 +576,7 @@
 SanitizerConstants.setDerivedConstants();
 
 // Freeze it blocking all accidental changes
-JSUtils.deepFreeze(SanitizerConstants);
+SanitizerConstants = JSUtils.deepFreeze(SanitizerConstants);
 
 /**
  * @class
diff --git a/lib/jsutils.js b/lib/jsutils.js
index 816646c..742cf85 100644
--- a/lib/jsutils.js
+++ b/lib/jsutils.js
@@ -5,6 +5,34 @@
 'use strict';
 require('./core-upgrade');
 
+var ReadOnlyMap = function ReadOnlyMap() {
+       /* the actual data will be stored in a private scope variable */
+};
+ReadOnlyMap.prototype.clear =
+ReadOnlyMap.prototype['delete'] =
+ReadOnlyMap.prototype.set = function() {
+       throw new TypeError("Mutation attempted on read-only map.");
+};
+if (typeof Symbol !== 'undefined' && typeof Symbol.iterator !== 'undefined') {
+       Object.defineProperty(ReadOnlyMap.prototype, Symbol.iterator, {
+               get: function() { return this.entries; }
+       });
+}
+
+var ReadOnlySet = function ReadOnlySet() {
+       /* the actual data will be stored in a private scope variable */
+};
+ReadOnlySet.prototype.add =
+ReadOnlySet.prototype.clear =
+ReadOnlyMap.prototype['delete'] = function() {
+       throw new TypeError("Mutation attempted on read-only set.");
+};
+if (typeof Symbol !== 'undefined' && typeof Symbol.iterator !== 'undefined') {
+       Object.defineProperty(ReadOnlySet.prototype, Symbol.iterator, {
+               get: function() { return this.values; }
+       });
+}
+
 var JSUtils = {
 
        // in ES7 it should be `new Map(Object.entries(obj))`
@@ -14,29 +42,90 @@
                }));
        },
 
+       // ES6 maps/sets are still writable even when frozen, because they
+       // store data inside the object linked from an internal slot.
+       // This returns an actual read-only Map.
+       readOnlyMap: function(it, freezeEntries) {
+               // Allow `it` to be an iterable, as well as a map.
+               if (!(it instanceof Map)) { it = new Map(it); }
+               /* Copy `it`, to prevent later mutation, and (optionally)
+                * freeze keys and values along the way. */
+               var freeze = function(v) {
+                       return freezeEntries ? JSUtils.deepFreeze(v) : v;
+               };
+               var m = new Map();
+               it.forEach(function(val, key) { m.set(freeze(key), 
freeze(val)); });
+               // Now make our read-only map object.
+               var ro = new ReadOnlyMap();
+               Object.defineProperties(ro, {
+                       constructor: { value: Map /* a little white lie */ },
+                       entries: { value: function() { return m.entries(); } },
+                       forEach: { value: function(f, t) { return m.forEach(f, 
t); } },
+                       get: { value: function(k) { return m.get(k); } },
+                       has: { value: function(k) { return m.has(k); } },
+                       keys: { value: function() { return m.keys(); } },
+                       size: { get: function() { return m.size; } },
+                       values: { value: function() { return m.values(); } },
+                       toString: { value: function() { return m.toString(); } 
},
+               });
+               Object.freeze(ro);
+               return ro;
+       },
+       // This returns an actual read-only Set.
+       readOnlySet: function(it, freezeEntries) {
+               // Allow `it` to be an iterable, as well as a set.
+               if (!(it instanceof Set)) { it = new Set(it); }
+               /* Copy `it`, to prevent later mutation, and (optionally)
+                * freeze keys and values along the way. */
+               var freeze = function(v) {
+                       return freezeEntries ? JSUtils.deepFreeze(v) : v;
+               };
+               var s = new Set();
+               it.forEach(function(val) { s.add(freeze(val)); });
+               // Now make our read-only set object.
+               var ro = new ReadOnlySet();
+               Object.defineProperties(ro, {
+                       constructor: { value: Set /* a little white lie */ },
+                       entries: { value: function() { return s.entries(); } },
+                       forEach: { value: function(f, t) { return s.forEach(f, 
t); } },
+                       has: { value: function(k) { return s.has(k); } },
+                       keys: { value: function() { return s.keys(); } },
+                       size: { get: function() { return s.size; } },
+                       values: { value: function() { return s.values(); } },
+                       toString: { value: function() { return s.toString(); } 
},
+               });
+               Object.freeze(ro);
+               return ro;
+       },
+
        // Deep-freeze an object
        // See 
https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/freeze
        deepFreeze: function(o) {
                if ( o === undefined ) {
-                       return;
+                       return o;
                } else if ( !(o instanceof Object) ) {
-                       return;
+                       return o;
                } else if ( Object.isFrozen(o) ) {
-                       return;
+                       return o;
+               } else if (o instanceof Map) {
+                       return JSUtils.readOnlyMap(o, true);
+               } else if (o instanceof Set) {
+                       return JSUtils.readOnlySet(o, true);
                }
 
-               Object.freeze(o); // First freeze the object.
                for (var propKey in o) {
-                       var prop = o[propKey];
-                       if (!o.hasOwnProperty(propKey) || !(prop instanceof 
Object) || Object.isFrozen(prop)) {
-                               // If the object is on the prototype, not an 
object, or is already frozen,
+                       var desc = Object.getOwnPropertyDescriptor(o, propKey);
+                       if (!desc || !(desc.value instanceof Object) || 
Object.isFrozen(desc.value)) {
+                               // If the object is on the prototype, is a 
getter, not an object, or is already frozen,
                                // skip it. Note that this might leave an 
unfrozen reference somewhere in the
                                // object if there is an already frozen object 
containing an unfrozen object.
                                continue;
                        }
-
-                       this.deepFreeze(prop); // Recursively call deepFreeze.
+                       // Recursively call deepFreeze.
+                       o[propKey] = JSUtils.deepFreeze(desc.value);
                }
+               Object.freeze(o); // Now freeze the object.
+               return o;
        },
 
        // Convert a counter to a Base64 encoded string.
diff --git a/lib/mediawiki.ParsoidConfig.js b/lib/mediawiki.ParsoidConfig.js
index f1fa168..baa6d5b 100644
--- a/lib/mediawiki.ParsoidConfig.js
+++ b/lib/mediawiki.ParsoidConfig.js
@@ -59,7 +59,7 @@
        }
 
        // ParsoidConfig is used across requests. Freeze it to avoid mutation.
-       JSUtils.deepFreeze(this);
+       return JSUtils.deepFreeze(this);
 }
 
 /**
diff --git a/lib/mediawiki.WikiConfig.js b/lib/mediawiki.WikiConfig.js
index 749c2ca..9a7bf67 100644
--- a/lib/mediawiki.WikiConfig.js
+++ b/lib/mediawiki.WikiConfig.js
@@ -10,7 +10,7 @@
 var request = require('request');
 
 // Make sure our base config is never modified
-JSUtils.deepFreeze(baseConfig);
+baseConfig = JSUtils.deepFreeze(baseConfig);
 
 
 /**
diff --git a/lib/mediawiki.wikitext.constants.js 
b/lib/mediawiki.wikitext.constants.js
index ef08550..c0c8e0d 100644
--- a/lib/mediawiki.wikitext.constants.js
+++ b/lib/mediawiki.wikitext.constants.js
@@ -301,7 +301,7 @@
 });
 
 // Freeze constants to prevent accidental changes
-JSUtils.deepFreeze(WikitextConstants);
+WikitextConstants = JSUtils.deepFreeze(WikitextConstants);
 
 if (typeof module === "object") {
        module.exports.WikitextConstants = WikitextConstants;
diff --git a/lib/wts.SerializerState.js b/lib/wts.SerializerState.js
index a16c9b7..482ebfd 100644
--- a/lib/wts.SerializerState.js
+++ b/lib/wts.SerializerState.js
@@ -70,7 +70,8 @@
  *    Stack used to enforce single-line context
  * ********************************************************************* */
 
-var initialState = {
+// Freeze to ensure the initialState is never modified
+var initialState = JSUtils.deepFreeze({
        rtTesting: true,
        sep: {},
        onSOL: true,
@@ -84,10 +85,7 @@
        wteHandlerStack: [],
        // XXX: replace with output buffering per line
        currLine: null
-};
-
-// Make sure the initialState is never modified
-JSUtils.deepFreeze(initialState);
+});
 
 // Stack and helpers to enforce single-line context while serializing
 function SingleLineContext() {

-- 
To view, visit https://gerrit.wikimedia.org/r/207816
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ia16738e88aa3ee9c46fd71da50ce66b8917a07d3
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/services/parsoid
Gerrit-Branch: master
Gerrit-Owner: Cscott <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to