Browse Source

fix: use jsonDump instead of JSON.stringify

close #295

Some sites overrides Array#toJSON, leading to invalid JSON string
built by `JSON.stringify`. This commit creates a `jsonDump` function
to serialize JSON strings and ignores `toJSON` property.
Gerald 8 years ago
parent
commit
6b546238d3

+ 68 - 12
src/injected/web/helpers.js → src/injected/utils/helpers.js

@@ -1,6 +1,11 @@
-import { Promise } from '../utils';
+// cache native properties to avoid being overridden, see violentmonkey/violentmonkey#151
+// eslint-disable-next-line no-restricted-properties
+export const {
+  console, CustomEvent, Promise, isFinite,
+} = window;
 
 const arrayProto = Array.prototype;
+const objectProto = Object.prototype;
 
 const bindThis = func => (thisObj, ...args) => func.apply(thisObj, args);
 
@@ -14,12 +19,17 @@ export const includes = arrayProto.includes
   ? bindThis(arrayProto.includes)
   : (arr, item) => indexOf(arr, item) >= 0;
 
-export const toString = bindThis(Object.prototype.toString);
+export const toString = bindThis(objectProto.toString);
+const numberToString = bindThis(Number.prototype.toString);
+const stringSlice = bindThis(String.prototype.slice);
+const stringCharCodeAt = bindThis(String.prototype.charCodeAt);
+const { fromCharCode } = String;
 
+export const { keys } = Object;
 export const assign = Object.assign || ((obj, ...args) => {
   forEach(args, arg => {
     if (arg) {
-      forEach(Object.keys(arg), key => {
+      forEach(keys(arg), key => {
         obj[key] = arg[key];
       });
     }
@@ -27,6 +37,8 @@ export const assign = Object.assign || ((obj, ...args) => {
   return obj;
 });
 
+export const isArray = obj => toString(obj) === '[object Array]';
+
 export function encodeBody(body) {
   const cls = getType(body);
   let result;
@@ -57,7 +69,7 @@ export function encodeBody(body) {
         let value = '';
         const array = new Uint8Array(reader.result);
         for (let i = 0; i < array.length; i += bufsize) {
-          value += String.fromCharCode.apply(null, array.subarray(i, i + bufsize));
+          value += fromCharCode(...array.subarray(i, i + bufsize));
         }
         resolve({
           cls,
@@ -72,7 +84,7 @@ export function encodeBody(body) {
   } else if (body) {
     result = {
       cls,
-      value: JSON.stringify(body),
+      value: jsonDump(body),
     };
   }
   return Promise.resolve(result);
@@ -98,21 +110,65 @@ export function utf8decode(utftext) {
   let c2 = 0;
   let c3 = 0;
   while (i < utftext.length) {
-    c1 = utftext.charCodeAt(i);
+    c1 = stringCharCodeAt(utftext, i);
     if (c1 < 128) {
-      string += String.fromCharCode(c1);
+      string += fromCharCode(c1);
       i += 1;
     } else if (c1 > 191 && c1 < 224) {
-      c2 = utftext.charCodeAt(i + 1);
-      string += String.fromCharCode(((c1 & 31) << 6) | (c2 & 63));
+      c2 = stringCharCodeAt(utftext, i + 1);
+      string += fromCharCode(((c1 & 31) << 6) | (c2 & 63));
       i += 2;
     } else {
-      c2 = utftext.charCodeAt(i + 1);
-      c3 = utftext.charCodeAt(i + 2);
-      string += String.fromCharCode(((c1 & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
+      c2 = stringCharCodeAt(utftext, i + 1);
+      c3 = stringCharCodeAt(utftext, i + 2);
+      string += fromCharCode(((c1 & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
       i += 3;
     }
   }
   return string;
   /* eslint-enable no-bitwise */
 }
+
+// Reference: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/JSON#Polyfill
+const escMap = {
+  '"': '\\"',
+  '\\': '\\\\',
+  '\b': '\\b',
+  '\f': '\\f',
+  '\n': '\\n',
+  '\r': '\\r',
+  '\t': '\\t',
+};
+const escRE = /[\\"\u0000-\u001F\u2028\u2029]/g;
+const escFunc = m => escMap[m] || `\\u${stringSlice(numberToString(stringCharCodeAt(m, 0) + 0x10000, 16), 1)}`;
+export const jsonLoad = JSON.parse;
+export function jsonDump(value) {
+  if (value == null) return 'null';
+  const type = typeof value;
+  if (type === 'number') {
+    return isFinite(value) ? `${value}` : 'null';
+  }
+  if (type === 'boolean') return `${value}`;
+  if (type === 'object') {
+    if (isArray(value)) {
+      let res = '[';
+      forEach(value, (item, i) => {
+        if (i) res += ',';
+        res += jsonDump(item);
+      });
+      res += ']';
+      return res;
+    }
+    if (toString(value) === '[object Object]') {
+      let res = '{';
+      forEach(keys(value), (key, i) => {
+        if (i) res += ',';
+        res += `${jsonDump(key)}:${jsonDump(value[key])}`;
+      });
+      res += '}';
+      return res;
+    }
+  }
+  const escaped = `${value}`.replace(escRE, escFunc);
+  return `"${escaped}"`;
+}

+ 5 - 6
src/injected/utils.js → src/injected/utils/index.js

@@ -1,11 +1,10 @@
-export { sendMessage, request, throttle } from 'src/common';
+import { CustomEvent, jsonDump, jsonLoad } from './helpers';
 
-// cache native properties to avoid being overridden, see violentmonkey/violentmonkey#151
-export const { console, CustomEvent, Promise } = window;
+export { sendMessage, request, throttle } from 'src/common';
 
 export function postData(destId, data) {
   // Firefox issue: data must be stringified to avoid cross-origin problem
-  const e = new CustomEvent(destId, { detail: JSON.stringify(data) });
+  const e = new CustomEvent(destId, { detail: jsonDump(data) });
   document.dispatchEvent(e);
 }
 
@@ -18,7 +17,7 @@ export function inject(code) {
       span.id = domId;
       document.documentElement.appendChild(span);
     };
-    injectViaText(`(${detect.toString()})(${JSON.stringify(id)})`);
+    injectViaText(`(${detect.toString()})(${jsonDump(id)})`);
     const span = document.querySelector(`#${id}`);
     if (span) {
       span.parentNode.removeChild(span);
@@ -67,7 +66,7 @@ export function getUniqId() {
 
 export function bindEvents(srcId, destId, handle) {
   document.addEventListener(srcId, e => {
-    const data = JSON.parse(e.detail);
+    const data = jsonLoad(e.detail);
     handle(data);
   }, false);
   return data => { postData(destId, data); };

+ 1 - 1
src/injected/web/bridge.js

@@ -1,4 +1,4 @@
-import { noop } from './helpers';
+import { noop } from '../utils/helpers';
 
 export default {
   load: noop,

+ 9 - 4
src/injected/web/index.js

@@ -1,5 +1,10 @@
-import { getUniqId, bindEvents, Promise, attachFunction, console } from '../utils';
-import { includes, forEach, map, utf8decode } from './helpers';
+import {
+  getUniqId, bindEvents, attachFunction,
+} from '../utils';
+import {
+  includes, forEach, map, utf8decode, jsonDump, jsonLoad,
+  Promise, console,
+} from '../utils/helpers';
 import bridge from './bridge';
 import { onRequestCreate, onRequestStart, onRequestCallback } from './requests';
 import {
@@ -158,13 +163,13 @@ function wrapGM(script, code, cache) {
   if (includes(grant, 'window.close')) gm.window.close = () => { bridge.post({ cmd: 'TabClose' }); };
   const resources = script.meta.resources || {};
   const dataEncoders = {
-    o: val => JSON.stringify(val),
+    o: val => jsonDump(val),
     '': val => val.toString(),
   };
   const dataDecoders = {
     n: val => Number(val),
     b: val => val === 'true',
-    o: val => JSON.parse(val),
+    o: val => jsonLoad(val),
     '': val => val,
   };
   const pathMap = script.custom.pathMap || {};

+ 2 - 2
src/injected/web/requests.js

@@ -1,4 +1,4 @@
-import { includes, encodeBody } from './helpers';
+import { includes, encodeBody, jsonLoad } from '../utils/helpers';
 import bridge from './bridge';
 
 const map = {};
@@ -55,7 +55,7 @@ function parseData(req, details) {
     }
   } else if (details.responseType === 'json') {
     // json
-    return JSON.parse(req.data.response);
+    return jsonLoad(req.data.response);
   } else {
     // text
     return req.data.response;

+ 2 - 0
test/index.js

@@ -1,3 +1,5 @@
 import './polyfill';
+import './common';
 import './background/tester';
 import './background/script';
+import './injected/helpers';

+ 25 - 0
test/injected/helpers.js

@@ -0,0 +1,25 @@
+import test from 'tape';
+import { jsonDump } from 'src/injected/utils/helpers';
+
+test('jsonDump', t => {
+  // eslint-disable-next-line no-restricted-syntax
+  for (const obj of [
+    1,
+    null,
+    false,
+    'abc',
+    {},
+    [],
+    [1, 2, 3],
+    {
+      a: 1, b: '2', c: true, d: 'aaa',
+    },
+    {
+      a: [1, 2, 3],
+      b: { a: 'b' },
+    },
+  ]) {
+    t.equal(jsonDump(obj), JSON.stringify(obj));
+  }
+  t.end();
+});

+ 2 - 0
test/polyfill.js

@@ -1,5 +1,7 @@
 import tldRules from 'tldjs/rules.json';
 
+global.window = global;
+
 global.browser = {
   storage: {
     local: {