diff --git a/superset/assets/spec/javascripts/utils/safeStringify_spec.ts b/superset/assets/spec/javascripts/utils/safeStringify_spec.ts new file mode 100644 index 000000000000..6a019a2c4a69 --- /dev/null +++ b/superset/assets/spec/javascripts/utils/safeStringify_spec.ts @@ -0,0 +1,110 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { safeStringify } from '../../../src/utils/safeStringify'; + +class Noise { + public next: Noise; +} + +describe('Stringify utility testing', () => { + it('correctly parses a simple object just like JSON', () => { + const noncircular = { + b: 'foo', + c: 'bar', + d: [ + { + e: 'hello', + f: ['world'], + }, + { + e: 'hello', + f: ['darkness', 'my', 'old', 'friend'], + }, + ], + }; + expect(safeStringify(noncircular)).toEqual(JSON.stringify(noncircular)); + // Checking that it works with quick-deepish-copies as well. + expect(JSON.parse(safeStringify(noncircular))).toEqual(JSON.parse(JSON.stringify(noncircular))); + }); + + it('handles simple circular json as expected', () => { + const ping = new Noise(); + const pong = new Noise(); + const pang = new Noise(); + ping.next = pong; + pong.next = ping; + + // ping.next is pong (the circular reference) now + const safeString = safeStringify(ping); + ping.next = pang; + + // ping.next is pang now, which has no circular reference, so it's safe to use JSON.stringify + const ordinaryString = JSON.stringify(ping); + expect(safeString).toEqual(ordinaryString); + }); + + it('creates a parseable object even when the input is circular', () => { + const ping = new Noise(); + const pong = new Noise(); + ping.next = pong; + pong.next = ping; + + const newNoise: Noise = JSON.parse(safeStringify(ping)); + expect(newNoise).toBeTruthy(); + expect(newNoise.next).toEqual({}); + }); + + it('does not remove noncircular duplicates', () => { + const a = { + foo: 'bar', + }; + + const repeating = { + first: a, + second: a, + third: a, + }; + + expect(safeStringify(repeating)).toEqual(JSON.stringify(repeating)); + }); + + it('does not remove nodes with empty objects', () => { + const emptyObjectValues = { + a: {}, + b: 'foo', + c: { + d: 'good data here', + e: {}, + }, + }; + expect(safeStringify(emptyObjectValues)).toEqual(JSON.stringify(emptyObjectValues)); + }); + + it('does not remove nested same keys', () => { + const nestedKeys = { + a: 'b', + c: { + a: 'd', + x: 'y', + }, + }; + + expect(safeStringify(nestedKeys)).toEqual(JSON.stringify(nestedKeys)); + }); +}); diff --git a/superset/assets/src/explore/exploreUtils.js b/superset/assets/src/explore/exploreUtils.js index 514851539de0..ec3496aea4cd 100644 --- a/superset/assets/src/explore/exploreUtils.js +++ b/superset/assets/src/explore/exploreUtils.js @@ -19,6 +19,7 @@ /* eslint camelcase: 0 */ import URI from 'urijs'; import { availableDomains } from '../utils/hostNamesConfig'; +import { safeStringify } from '../utils/safeStringify'; const MAX_URL_LENGTH = 8000; @@ -71,7 +72,7 @@ export function getExploreLongUrl(formData, endpointType, allowOverflow = true, Object.keys(extraSearch).forEach((key) => { search[key] = extraSearch[key]; }); - search.form_data = JSON.stringify(formData); + search.form_data = safeStringify(formData); if (endpointType === 'standalone') { search.standalone = 'true'; } diff --git a/superset/assets/src/utils/safeStringify.ts b/superset/assets/src/utils/safeStringify.ts new file mode 100644 index 000000000000..230dd35bd188 --- /dev/null +++ b/superset/assets/src/utils/safeStringify.ts @@ -0,0 +1,45 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * A Stringify function that will not crash when it runs into circular JSON references, + * unlike JSON.stringify. Any circular references are simply omitted, as if there had + * been no data present + * @param object any JSON object to be stringified + */ +export function safeStringify(object: any): string { + const cache = new Set(); + return JSON.stringify(object, (key, value) => { + if (typeof value === 'object' && value !== null) { + if (cache.has(value)) { + // We've seen this object before + try { + // Quick deep copy to duplicate if this is a repeat rather than a circle. + return JSON.parse(JSON.stringify(value)); + } catch (err) { + // Discard key if value cannot be duplicated. + return; + } + } + // Store the value in our cache. + cache.add(value); + } + return value; + }); +}