fix(CRUD/listviews): Errors with rison and search strings using special characters (#18056)

* fix errors with rison and useQueryParams

* add test for encode/decode

* add rison link and make test case more readable

Co-authored-by: Corbin Robb <corbin@Corbins-MacBook-Pro.local>
This commit is contained in:
Corbin Robb 2022-02-15 15:08:36 -07:00 committed by GitHub
parent 97a879ef27
commit c8df84985c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 59 additions and 9 deletions

View File

@ -51,8 +51,7 @@ export default function SearchFilter({
const [value, setValue] = useState(initialValue || ''); const [value, setValue] = useState(initialValue || '');
const handleSubmit = () => { const handleSubmit = () => {
if (value) { if (value) {
// encode plus signs to prevent them from being converted into a space onSubmit(value.trim());
onSubmit(value.trim().replace(/\+/g, '%2B'));
} }
}; };
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {

View File

@ -46,10 +46,17 @@ import {
} from './types'; } from './types';
// Define custom RisonParam for proper encoding/decoding; note that // 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<string, any> = { const RisonParam: QueryParamConfig<string, any> = {
encode: (data?: any | null) => 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[]) => decode: (dataStr?: string | string[]) =>
dataStr === undefined || Array.isArray(dataStr) dataStr === undefined || Array.isArray(dataStr)
? undefined ? undefined

View File

@ -75,7 +75,7 @@ const fetchTimeRange = async (
timeRange: string, timeRange: string,
endpoints?: TimeRangeEndpoints, endpoints?: TimeRangeEndpoints,
) => { ) => {
const query = rison.encode(timeRange); const query = rison.encode_uri(timeRange);
const endpoint = `/api/v1/time_range/?q=${query}`; const endpoint = `/api/v1/time_range/?q=${query}`;
try { try {
const response = await SupersetClient.get({ endpoint }); const response = await SupersetClient.get({ endpoint });

View File

@ -675,7 +675,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
const loadDashboardOptions = useMemo( const loadDashboardOptions = useMemo(
() => () =>
(input = '', page: number, pageSize: number) => { (input = '', page: number, pageSize: number) => {
const query = rison.encode({ const query = rison.encode_uri({
filter: input, filter: input,
page, page,
page_size: pageSize, page_size: pageSize,
@ -749,7 +749,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
const loadChartOptions = useMemo( const loadChartOptions = useMemo(
() => () =>
(input = '', page: number, pageSize: number) => { (input = '', page: number, pageSize: number) => {
const query = rison.encode({ const query = rison.encode_uri({
filter: input, filter: input,
page, page,
page_size: pageSize, page_size: pageSize,

View File

@ -147,7 +147,7 @@ export function useListViewResource<D extends object = any>(
: value, : value,
})); }));
const queryParams = rison.encode({ const queryParams = rison.encode_uri({
order_column: sortBy[0].id, order_column: sortBy[0].id,
order_direction: sortBy[0].desc ? 'desc' : 'asc', order_direction: sortBy[0].desc ? 'desc' : 'asc',
page: pageIndex, page: pageIndex,

View File

@ -16,6 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import rison from 'rison';
import { import {
isNeedsPassword, isNeedsPassword,
isAlreadyExists, isAlreadyExists,
@ -171,3 +172,17 @@ test('does not ask for password when the import type is wrong', () => {
}; };
expect(hasTerminalValidation(error.errors)).toBe(true); expect(hasTerminalValidation(error.errors)).toBe(true);
}); });
test('successfully modified rison to encode correctly', () => {
const problemCharacters = '& # ? ^ { } [ ] | " = + `';
problemCharacters.split(' ').forEach(char => {
const testObject = { test: char };
const actualEncoding = rison.encode(testObject);
const expectedEncoding = `(test:'${char}')`; // Ex: (test:'&')
expect(actualEncoding).toEqual(expectedEncoding);
expect(rison.decode(actualEncoding)).toEqual(testObject);
});
});

View File

@ -33,6 +33,35 @@ import { FetchDataConfig } from 'src/components/ListView';
import SupersetText from 'src/utils/textUtils'; import SupersetText from 'src/utils/textUtils';
import { Dashboard, Filters } from './types'; 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 (https://github.com/Nanonid/rison), rison is licensed under the MIT license.
(() => {
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');
})();
const createFetchResourceMethod = const createFetchResourceMethod =
(method: string) => (method: string) =>
( (
@ -43,7 +72,7 @@ const createFetchResourceMethod =
) => ) =>
async (filterValue = '', page: number, pageSize: number) => { async (filterValue = '', page: number, pageSize: number) => {
const resourceEndpoint = `/api/v1/${resource}/${method}/${relation}`; const resourceEndpoint = `/api/v1/${resource}/${method}/${relation}`;
const queryParams = rison.encode({ const queryParams = rison.encode_uri({
filter: filterValue, filter: filterValue,
page, page,
page_size: pageSize, page_size: pageSize,