From 76ce8b11f437c7a08a32c792b4506072629e3244 Mon Sep 17 00:00:00 2001 From: Corbin Robb Date: Fri, 14 Jan 2022 10:04:16 -0700 Subject: [PATCH 1/3] fix errors with rison and useQueryParams --- .../components/ListView/Filters/Search.tsx | 3 +- .../src/components/ListView/utils.ts | 11 +++++-- .../DateFilterControl/DateFilterLabel.tsx | 2 +- .../src/views/CRUD/alert/AlertReportModal.tsx | 4 +-- superset-frontend/src/views/CRUD/hooks.ts | 2 +- superset-frontend/src/views/CRUD/utils.tsx | 33 ++++++++++++++++++- 6 files changed, 46 insertions(+), 9 deletions(-) diff --git a/superset-frontend/src/components/ListView/Filters/Search.tsx b/superset-frontend/src/components/ListView/Filters/Search.tsx index 482fac17d4b7a..fd61cba5cc79f 100644 --- a/superset-frontend/src/components/ListView/Filters/Search.tsx +++ b/superset-frontend/src/components/ListView/Filters/Search.tsx @@ -51,8 +51,7 @@ export default function SearchFilter({ const [value, setValue] = useState(initialValue || ''); const handleSubmit = () => { if (value) { - // encode plus signs to prevent them from being converted into a space - onSubmit(value.trim().replace(/\+/g, '%2B')); + onSubmit(value.trim()); } }; const handleChange = (e: React.ChangeEvent) => { diff --git a/superset-frontend/src/components/ListView/utils.ts b/superset-frontend/src/components/ListView/utils.ts index 3ce370d054867..346bde0982fc3 100644 --- a/superset-frontend/src/components/ListView/utils.ts +++ b/superset-frontend/src/components/ListView/utils.ts @@ -46,10 +46,17 @@ import { } from './types'; // Define custom RisonParam for proper encoding/decoding; note that -// plus symbols should be encoded to avoid being converted into a space +// %, &, +, and # must be encoded to avoid breaking the url const RisonParam: QueryParamConfig = { encode: (data?: any | null) => - data === undefined ? undefined : rison.encode(data).replace(/\+/g, '%2B'), + data === undefined + ? undefined + : rison + .encode(data) + .replace(/%/g, '%25') + .replace(/&/g, '%26') + .replace(/\+/g, '%2B') + .replace(/#/g, '%23'), decode: (dataStr?: string | string[]) => dataStr === undefined || Array.isArray(dataStr) ? undefined diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterLabel.tsx b/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterLabel.tsx index e5324a926bdd3..80f287295989f 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterLabel.tsx +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterLabel.tsx @@ -75,7 +75,7 @@ const fetchTimeRange = async ( timeRange: string, endpoints?: TimeRangeEndpoints, ) => { - const query = rison.encode(timeRange); + const query = rison.encode_uri(timeRange); const endpoint = `/api/v1/time_range/?q=${query}`; try { const response = await SupersetClient.get({ endpoint }); diff --git a/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx b/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx index 65020aeabf1c9..44c13852f8177 100644 --- a/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx +++ b/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx @@ -674,7 +674,7 @@ const AlertReportModal: FunctionComponent = ({ const loadDashboardOptions = useMemo( () => (input = '', page: number, pageSize: number) => { - const query = rison.encode({ + const query = rison.encode_uri({ filter: input, page, page_size: pageSize, @@ -748,7 +748,7 @@ const AlertReportModal: FunctionComponent = ({ const loadChartOptions = useMemo( () => (input = '', page: number, pageSize: number) => { - const query = rison.encode({ + const query = rison.encode_uri({ filter: input, page, page_size: pageSize, diff --git a/superset-frontend/src/views/CRUD/hooks.ts b/superset-frontend/src/views/CRUD/hooks.ts index 2c9c2f9930e05..ae4b041f66878 100644 --- a/superset-frontend/src/views/CRUD/hooks.ts +++ b/superset-frontend/src/views/CRUD/hooks.ts @@ -147,7 +147,7 @@ export function useListViewResource( : value, })); - const queryParams = rison.encode({ + const queryParams = rison.encode_uri({ order_column: sortBy[0].id, order_direction: sortBy[0].desc ? 'desc' : 'asc', page: pageIndex, diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx index 64a92e08c63de..cb5c01db55d0b 100644 --- a/superset-frontend/src/views/CRUD/utils.tsx +++ b/superset-frontend/src/views/CRUD/utils.tsx @@ -33,6 +33,37 @@ import { FetchDataConfig } from 'src/components/ListView'; import SupersetText from 'src/utils/textUtils'; import { Dashboard, Filters } from './types'; +// Modifies the rison encoding slightly to match the backend's +// rison encoding/decoding. Applies globally. Code pulled from rison.js +const fixRisonEncodeDecode = () => { + const risonRef: { + not_idchar: string; + not_idstart: string; + id_ok: RegExp; + next_id: RegExp; + } = rison as any; + + const l = []; + for (let hi = 0; hi < 16; hi += 1) { + for (let lo = 0; lo < 16; lo += 1) { + if (hi + lo === 0) continue; + const c = String.fromCharCode(hi * 16 + lo); + if (!/\w|[-_./~]/.test(c)) + l.push(`\\u00${hi.toString(16)}${lo.toString(16)}`); + } + } + + risonRef.not_idchar = l.join(''); + risonRef.not_idstart = '-0123456789'; + + const idrx = `[^${risonRef.not_idstart}${risonRef.not_idchar}][^${risonRef.not_idchar}]*`; + + risonRef.id_ok = new RegExp(`^${idrx}$`); + risonRef.next_id = new RegExp(idrx, 'g'); +}; + +fixRisonEncodeDecode(); + const createFetchResourceMethod = (method: string) => ( @@ -43,7 +74,7 @@ const createFetchResourceMethod = ) => async (filterValue = '', page: number, pageSize: number) => { const resourceEndpoint = `/api/v1/${resource}/${method}/${relation}`; - const queryParams = rison.encode({ + const queryParams = rison.encode_uri({ filter: filterValue, page, page_size: pageSize, From d88c4fed1e03dff55f3668f3cc399966c5834e76 Mon Sep 17 00:00:00 2001 From: Corbin Robb Date: Thu, 3 Feb 2022 11:33:16 -0700 Subject: [PATCH 2/3] add test for encode/decode --- .../src/views/CRUD/utils.test.tsx | 19 +++++++++++++++++++ superset-frontend/src/views/CRUD/utils.tsx | 6 ++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/superset-frontend/src/views/CRUD/utils.test.tsx b/superset-frontend/src/views/CRUD/utils.test.tsx index 2b95319d85d5b..e303c771883d8 100644 --- a/superset-frontend/src/views/CRUD/utils.test.tsx +++ b/superset-frontend/src/views/CRUD/utils.test.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import rison from 'rison'; import { isNeedsPassword, isAlreadyExists, @@ -171,3 +172,21 @@ test('does not ask for password when the import type is wrong', () => { }; expect(hasTerminalValidation(error.errors)).toBe(true); }); + +test('successfully modified rison to encode correctly', () => { + const problemCharacters = '& # ? ^ { } [ ] | " = + `'; + + const testObject = problemCharacters.split(' ').reduce((a, c) => { + // eslint-disable-next-line no-param-reassign + a[c] = c; + return a; + }, {}); + + const actualEncoding = rison.encode(testObject); + + const expectedEncoding = + "('\"':'\"','#':'#','&':'&','+':'+','=':'=','?':'?','[':'[',']':']','^':'^','`':'`','{':'{','|':'|','}':'}')"; + + expect(actualEncoding).toEqual(expectedEncoding); + expect(rison.decode(actualEncoding)).toEqual(testObject); +}); diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx index cb5c01db55d0b..679409dd7b94b 100644 --- a/superset-frontend/src/views/CRUD/utils.tsx +++ b/superset-frontend/src/views/CRUD/utils.tsx @@ -35,7 +35,7 @@ import { Dashboard, Filters } from './types'; // Modifies the rison encoding slightly to match the backend's // rison encoding/decoding. Applies globally. Code pulled from rison.js -const fixRisonEncodeDecode = () => { +(() => { const risonRef: { not_idchar: string; not_idstart: string; @@ -60,9 +60,7 @@ const fixRisonEncodeDecode = () => { risonRef.id_ok = new RegExp(`^${idrx}$`); risonRef.next_id = new RegExp(idrx, 'g'); -}; - -fixRisonEncodeDecode(); +})(); const createFetchResourceMethod = (method: string) => From eee89756e00867dab1fe5248fbebd5be170c52d8 Mon Sep 17 00:00:00 2001 From: Corbin Robb Date: Tue, 15 Feb 2022 08:37:44 -0700 Subject: [PATCH 3/3] add rison link and make test case more readable --- .../src/views/CRUD/utils.test.tsx | 18 +++++++----------- superset-frontend/src/views/CRUD/utils.tsx | 4 ++-- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/superset-frontend/src/views/CRUD/utils.test.tsx b/superset-frontend/src/views/CRUD/utils.test.tsx index e303c771883d8..ce7139e82a99a 100644 --- a/superset-frontend/src/views/CRUD/utils.test.tsx +++ b/superset-frontend/src/views/CRUD/utils.test.tsx @@ -176,17 +176,13 @@ test('does not ask for password when the import type is wrong', () => { test('successfully modified rison to encode correctly', () => { const problemCharacters = '& # ? ^ { } [ ] | " = + `'; - const testObject = problemCharacters.split(' ').reduce((a, c) => { - // eslint-disable-next-line no-param-reassign - a[c] = c; - return a; - }, {}); + problemCharacters.split(' ').forEach(char => { + const testObject = { test: char }; - const actualEncoding = rison.encode(testObject); + const actualEncoding = rison.encode(testObject); + const expectedEncoding = `(test:'${char}')`; // Ex: (test:'&') - const expectedEncoding = - "('\"':'\"','#':'#','&':'&','+':'+','=':'=','?':'?','[':'[',']':']','^':'^','`':'`','{':'{','|':'|','}':'}')"; - - expect(actualEncoding).toEqual(expectedEncoding); - expect(rison.decode(actualEncoding)).toEqual(testObject); + expect(actualEncoding).toEqual(expectedEncoding); + expect(rison.decode(actualEncoding)).toEqual(testObject); + }); }); diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx index 679409dd7b94b..174e1aa493108 100644 --- a/superset-frontend/src/views/CRUD/utils.tsx +++ b/superset-frontend/src/views/CRUD/utils.tsx @@ -33,8 +33,8 @@ import { FetchDataConfig } from 'src/components/ListView'; import SupersetText from 'src/utils/textUtils'; import { Dashboard, Filters } from './types'; -// Modifies the rison encoding slightly to match the backend's -// rison encoding/decoding. Applies globally. Code pulled from rison.js +// Modifies the rison encoding slightly to match the backend's rison encoding/decoding. Applies globally. +// Code pulled from rison.js (https://github.com/Nanonid/rison), rison is licensed under the MIT license. (() => { const risonRef: { not_idchar: string;