diff --git a/.eslintrc.js b/.eslintrc.js index abfe5e0a6cc27..9b00135df5bac 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -355,13 +355,7 @@ module.exports = { settings: { // instructs import/no-extraneous-dependencies to treat certain modules // as core modules, even if they aren't listed in package.json - 'import/core-modules': [ - 'plugins', - 'legacy/ui', - 'uiExports', - // TODO: Remove once https://github.com/benmosher/eslint-plugin-import/issues/1374 is fixed - 'querystring', - ], + 'import/core-modules': ['plugins', 'legacy/ui', 'uiExports'], 'import/resolver': { '@kbn/eslint-import-resolver-kibana': { diff --git a/NOTICE.txt b/NOTICE.txt index 69be6db72cff2..33c1d535d7df3 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -218,28 +218,3 @@ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---- -This product includes code that was extracted from angular@1.3. -Original license: -The MIT License - -Copyright (c) 2010-2014 Google, Inc. http://angularjs.org - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - diff --git a/package.json b/package.json index c3762c2eabd28..5bf33f0ab0bcb 100644 --- a/package.json +++ b/package.json @@ -230,7 +230,7 @@ "prop-types": "15.6.0", "proxy-from-env": "1.0.0", "pug": "^2.0.4", - "querystring-browser": "1.0.4", + "query-string": "6.10.1", "raw-loader": "3.1.0", "react": "^16.12.0", "react-color": "^2.13.8", diff --git a/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js b/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js index e02c38494991a..da0b799b338ed 100755 --- a/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js +++ b/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js @@ -29,7 +29,6 @@ exports.getWebpackConfig = function(kibanaPath, projectRoot, config) { // Kibana defaults https://github.com/elastic/kibana/blob/6998f074542e8c7b32955db159d15661aca253d7/src/legacy/ui/ui_bundler_env.js#L30-L36 ui: fromKibana('src/legacy/ui/public'), test_harness: fromKibana('src/test_harness/public'), - querystring: 'querystring-browser', // Dev defaults for test bundle https://github.com/elastic/kibana/blob/6998f074542e8c7b32955db159d15661aca253d7/src/core_plugins/tests_bundle/index.js#L73-L78 ng_mock$: fromKibana('src/test_utils/public/ng_mock'), diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 230a229b36888..81d756f47d760 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -19,8 +19,7 @@ import { Request } from 'hapi'; import { merge } from 'lodash'; import { Socket } from 'net'; - -import querystring from 'querystring'; +import { stringify } from 'query-string'; import { schema } from '@kbn/config-schema'; @@ -55,7 +54,8 @@ function createKibanaRequestMock({ socket = new Socket(), routeTags, }: RequestFixtureOptions = {}) { - const queryString = querystring.stringify(query); + const queryString = stringify(query, { sort: false }); + return KibanaRequest.from( createRawRequestMock({ headers, diff --git a/src/core/utils/url.ts b/src/core/utils/url.ts index 67b379e729ca4..31de7e1814038 100644 --- a/src/core/utils/url.ts +++ b/src/core/utils/url.ts @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - -import { ParsedUrlQuery } from 'querystring'; +import { ParsedQuery } from 'query-string'; import { format as formatUrl, parse as parseUrl, UrlObject } from 'url'; /** @@ -33,7 +32,7 @@ export interface URLMeaningfulParts { protocol?: string | null; slashes?: boolean | null; port?: string | null; - query: ParsedUrlQuery; + query: ParsedQuery; } /** diff --git a/src/legacy/server/logging/log_format.js b/src/legacy/server/logging/log_format.js index 0e284df230ef4..ca1d756704dd0 100644 --- a/src/legacy/server/logging/log_format.js +++ b/src/legacy/server/logging/log_format.js @@ -20,10 +20,10 @@ import Stream from 'stream'; import moment from 'moment-timezone'; import { get, _ } from 'lodash'; +import queryString from 'query-string'; import numeral from '@elastic/numeral'; import chalk from 'chalk'; import stringify from 'json-stringify-safe'; -import querystring from 'querystring'; import applyFiltersToKeys from './apply_filters_to_keys'; import { inspect } from 'util'; import { logWithMetadata } from './log_with_metadata'; @@ -108,7 +108,7 @@ export default class TransformObjStream extends Stream.Transform { contentLength: contentLength, }; - const query = querystring.stringify(event.query); + const query = queryString.stringify(event.query, { sort: false }); if (query) data.req.url += '?' + query; data.message = data.req.method.toUpperCase() + ' '; diff --git a/src/legacy/ui/public/state_management/global_state.js b/src/legacy/ui/public/state_management/global_state.js index 955759e305950..d8ff38106b978 100644 --- a/src/legacy/ui/public/state_management/global_state.js +++ b/src/legacy/ui/public/state_management/global_state.js @@ -17,7 +17,6 @@ * under the License. */ -import { QueryString } from '../utils/query_string'; import { StateProvider } from './state'; import { uiModules } from '../modules'; import { createLegacyClass } from '../utils/legacy_class'; @@ -35,10 +34,6 @@ export function GlobalStateProvider(Private) { // if the url param is missing, write it back GlobalState.prototype._persistAcrossApps = true; - GlobalState.prototype.removeFromUrl = function(url) { - return QueryString.replaceParamInUrl(url, this._urlParam, null); - }; - return new GlobalState(); } diff --git a/src/legacy/ui/public/utils/query_string.js b/src/legacy/ui/public/utils/query_string.js deleted file mode 100644 index 5fbc6da67bc98..0000000000000 --- a/src/legacy/ui/public/utils/query_string.js +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. 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 { encodeQueryComponent } from '../../../utils'; - -export const QueryString = {}; - -/***** -/*** originally copied from angular, modified our purposes -/*****/ - -function tryDecodeURIComponent(value) { - try { - return decodeURIComponent(value); - } catch (e) { - // Ignore any invalid uri component - } // eslint-disable-line no-empty -} - -/** - * Parses an escaped url query string into key-value pairs. - * @returns {Object.} - */ -QueryString.decode = function(keyValue) { - const obj = {}; - let keyValueParts; - let key; - - (keyValue || '').split('&').forEach(function(keyValue) { - if (keyValue) { - keyValueParts = keyValue.split('='); - key = tryDecodeURIComponent(keyValueParts[0]); - if (key !== void 0) { - const val = keyValueParts[1] !== void 0 ? tryDecodeURIComponent(keyValueParts[1]) : true; - if (!obj[key]) { - obj[key] = val; - } else if (Array.isArray(obj[key])) { - obj[key].push(val); - } else { - obj[key] = [obj[key], val]; - } - } - } - }); - return obj; -}; - -/** - * Creates a queryString out of an object - * @param {Object} obj - * @return {String} - */ -QueryString.encode = function(obj) { - const parts = []; - const keys = Object.keys(obj).sort(); - keys.forEach(function(key) { - const value = obj[key]; - if (Array.isArray(value)) { - value.forEach(function(arrayValue) { - parts.push(QueryString.param(key, arrayValue)); - }); - } else { - parts.push(QueryString.param(key, value)); - } - }); - return parts.length ? parts.join('&') : ''; -}; - -QueryString.param = function(key, val) { - return ( - encodeQueryComponent(key, true) + (val === true ? '' : '=' + encodeQueryComponent(val, true)) - ); -}; - -/** - * Extracts the query string from a url - * @param {String} url - * @return {Object} - returns an object describing the start/end index of the url in the string. The indices will be - * the same if the url does not have a query string - */ -QueryString.findInUrl = function(url) { - let qsStart = url.indexOf('?'); - let hashStart = url.lastIndexOf('#'); - - if (hashStart === -1) { - // out of bounds - hashStart = url.length; - } - - if (qsStart === -1) { - qsStart = hashStart; - } - - return { - start: qsStart, - end: hashStart, - }; -}; - -QueryString.replaceParamInUrl = function(url, param, newVal) { - const loc = QueryString.findInUrl(url); - const parsed = QueryString.decode(url.substring(loc.start + 1, loc.end)); - - if (newVal != null) { - parsed[param] = newVal; - } else { - delete parsed[param]; - } - - const chars = url.split(''); - chars.splice(loc.start, loc.end - loc.start, '?' + QueryString.encode(parsed)); - return chars.join(''); -}; diff --git a/src/legacy/ui/ui_exports/ui_export_defaults.js b/src/legacy/ui/ui_exports/ui_export_defaults.js index 1cb23d2ad2a23..459559e84b1a7 100644 --- a/src/legacy/ui/ui_exports/ui_export_defaults.js +++ b/src/legacy/ui/ui_exports/ui_export_defaults.js @@ -30,7 +30,6 @@ export const UI_EXPORT_DEFAULTS = { ui: resolve(ROOT, 'src/legacy/ui/public'), __kibanaCore__$: resolve(ROOT, 'src/core/public'), test_harness: resolve(ROOT, 'src/test_harness/public'), - querystring: 'querystring-browser', moment$: resolve(ROOT, 'webpackShims/moment'), 'moment-timezone$': resolve(ROOT, 'webpackShims/moment-timezone'), }, diff --git a/src/legacy/utils/index.js b/src/legacy/utils/index.js index 2e6381b31ecee..a4c0cdf958fc2 100644 --- a/src/legacy/utils/index.js +++ b/src/legacy/utils/index.js @@ -21,7 +21,6 @@ export { BinderBase } from './binder'; export { BinderFor } from './binder_for'; export { deepCloneWithBuffers } from './deep_clone_with_buffers'; export { unset } from './unset'; -export { encodeQueryComponent } from './encode_query_component'; export { watchStdioForLine } from './watch_stdio_for_line'; export { IS_KIBANA_DISTRIBUTABLE } from './artifact_type'; export { IS_KIBANA_RELEASE } from './artifact_type'; diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index b3e966ddffa4c..bfa74392c14fb 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -21,11 +21,7 @@ import React, { CSSProperties, useCallback, useEffect, useRef, useState } from ' import { EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { debounce } from 'lodash'; - -// Node v5 querystring for browser. -// @ts-ignore -import * as qs from 'querystring-browser'; - +import { parse } from 'query-string'; import { EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useServicesContext, useEditorReadContext } from '../../../../contexts'; import { useUIAceKeyboardMode } from '../use_ui_ace_keyboard_mode'; @@ -51,6 +47,10 @@ export interface EditorProps { initialTextValue: string; } +interface QueryParams { + load_from: string; +} + const abs: CSSProperties = { position: 'absolute', top: '0', @@ -98,7 +98,8 @@ function EditorUI({ initialTextValue }: EditorProps) { const readQueryParams = () => { const [, queryString] = (window.location.hash || '').split('?'); - return qs.parse(queryString || ''); + + return parse(queryString || '', { sort: false }) as Required; }; const loadBufferFromRemote = (url: string) => { @@ -138,6 +139,7 @@ function EditorUI({ initialTextValue }: EditorProps) { window.addEventListener('hashchange', onHashChange); const initialQueryParams = readQueryParams(); + if (initialQueryParams.load_from) { loadBufferFromRemote(initialQueryParams.load_from); } else { diff --git a/src/plugins/console/public/lib/es/es.ts b/src/plugins/console/public/lib/es/es.ts index 52aba98d9e662..f11692e1befad 100644 --- a/src/plugins/console/public/lib/es/es.ts +++ b/src/plugins/console/public/lib/es/es.ts @@ -17,8 +17,8 @@ * under the License. */ -import { stringify as formatQueryString } from 'querystring'; import $ from 'jquery'; +import { stringify } from 'query-string'; const esVersion: string[] = []; @@ -35,7 +35,7 @@ export function send(method: string, path: string, data: any) { const wrappedDfd = $.Deferred(); // eslint-disable-line new-cap const options: JQuery.AjaxSettings = { - url: '../api/console/proxy?' + formatQueryString({ path, method }), + url: '../api/console/proxy?' + stringify({ path, method }, { sort: false }), data, contentType: getContentType(data), cache: false, diff --git a/src/plugins/data/public/query/timefilter/lib/parse_querystring.ts b/src/plugins/data/public/query/timefilter/lib/parse_querystring.ts index 467110b6f32ea..2220ad4eef1b7 100644 --- a/src/plugins/data/public/query/timefilter/lib/parse_querystring.ts +++ b/src/plugins/data/public/query/timefilter/lib/parse_querystring.ts @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - -import qs from 'querystring'; +import { parse } from 'query-string'; export function parseQueryString() { // window.location.search is an empty string @@ -27,5 +26,5 @@ export function parseQueryString() { return {}; } - return qs.parse(hrefSplit[1]); + return parse(hrefSplit[1], { sort: false }); } diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index 4551d0e63c4be..3b07674315dce 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -24,3 +24,4 @@ export * from './state_containers'; export * from './typed_json'; export { createGetterSetter, Get, Set } from './create_getter_setter'; export { distinctUntilChangedWithInitialValue } from './distinct_until_changed_with_initial_value'; +export { url } from './url'; diff --git a/src/plugins/kibana_utils/public/state_management/url/stringify_query_string.test.ts b/src/plugins/kibana_utils/common/url/encode_uri_query.test.ts similarity index 78% rename from src/plugins/kibana_utils/public/state_management/url/stringify_query_string.test.ts rename to src/plugins/kibana_utils/common/url/encode_uri_query.test.ts index 3ca6cb4214682..b600822946299 100644 --- a/src/plugins/kibana_utils/public/state_management/url/stringify_query_string.test.ts +++ b/src/plugins/kibana_utils/common/url/encode_uri_query.test.ts @@ -17,27 +17,10 @@ * under the License. */ -import { encodeUriQuery, stringifyQueryString } from './stringify_query_string'; +import { encodeUriQuery, encodeQuery } from './encode_uri_query'; -describe('stringifyQueryString', () => { - it('stringifyQueryString', () => { - expect( - stringifyQueryString({ - a: 'asdf1234asdf', - b: "-_.!~*'() -_.!~*'()", - c: ':@$, :@$,', - d: "&;=+# &;=+#'", - f: ' ', - g: 'null', - }) - ).toMatchInlineSnapshot( - `"a=asdf1234asdf&b=-_.!~*'()%20-_.!~*'()&c=:@$,%20:@$,&d=%26;%3D%2B%23%20%26;%3D%2B%23'&f=%20&g=null"` - ); - }); -}); - -describe('encodeUriQuery', function() { - it('should correctly encode uri query and not encode chars defined as pchar set in rfc3986', () => { +describe('encodeUriQuery', () => { + test('should correctly encode uri query and not encode chars defined as pchar set in rfc3986', () => { // don't encode alphanum expect(encodeUriQuery('asdf1234asdf')).toBe('asdf1234asdf'); @@ -63,3 +46,25 @@ describe('encodeUriQuery', function() { expect(encodeUriQuery('null')).toBe('null'); }); }); + +describe('encodeQuery', () => { + test('encodeQuery', () => { + expect( + encodeQuery({ + a: 'asdf1234asdf', + b: "-_.!~*'() -_.!~*'()", + c: ':@$, :@$,', + d: "&;=+# &;=+#'", + f: ' ', + g: 'null', + }) + ).toEqual({ + a: 'asdf1234asdf', + b: "-_.!~*'()%20-_.!~*'()", + c: ':@$,%20:@$,', + d: "%26;%3D%2B%23%20%26;%3D%2B%23'", + f: '%20', + g: 'null', + }); + }); +}); diff --git a/src/legacy/utils/encode_query_component.ts b/src/plugins/kibana_utils/common/url/encode_uri_query.ts similarity index 72% rename from src/legacy/utils/encode_query_component.ts rename to src/plugins/kibana_utils/common/url/encode_uri_query.ts index 698d11803649d..fb60f0ceff10f 100644 --- a/src/legacy/utils/encode_query_component.ts +++ b/src/plugins/kibana_utils/common/url/encode_uri_query.ts @@ -17,6 +17,9 @@ * under the License. */ +import { ParsedQuery } from 'query-string'; +import { transform } from 'lodash'; + /** * This method is intended for encoding *key* or *value* parts of query component. We need a custom * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be @@ -28,11 +31,27 @@ * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" * / "*" / "+" / "," / ";" / "=" */ -export function encodeQueryComponent(val: string, pctEncodeSpaces = false) { +export function encodeUriQuery(val: string, pctEncodeSpaces = false) { return encodeURIComponent(val) .replace(/%40/gi, '@') .replace(/%3A/gi, ':') .replace(/%24/g, '$') .replace(/%2C/gi, ',') + .replace(/%3B/gi, ';') .replace(/%20/g, pctEncodeSpaces ? '%20' : '+'); } + +export const encodeQuery = ( + query: ParsedQuery, + encodeFunction: (val: string, pctEncodeSpaces?: boolean) => string = encodeUriQuery +) => + transform(query, (result, value, key) => { + if (key) { + const singleValue = Array.isArray(value) ? value.join(',') : value; + + result[key] = encodeFunction( + singleValue === undefined || singleValue === null ? '' : singleValue, + true + ); + } + }); diff --git a/src/legacy/ui/public/utils/query_string.d.ts b/src/plugins/kibana_utils/common/url/index.ts similarity index 77% rename from src/legacy/ui/public/utils/query_string.d.ts rename to src/plugins/kibana_utils/common/url/index.ts index 959171443185e..7b74f07e598ee 100644 --- a/src/legacy/ui/public/utils/query_string.d.ts +++ b/src/plugins/kibana_utils/common/url/index.ts @@ -17,12 +17,9 @@ * under the License. */ -declare class QueryStringClass { - public decode(queryString: string): any; - public encode(obj: any): string; - public param(key: string, value: string): string; -} +import { encodeUriQuery, encodeQuery } from './encode_uri_query'; -declare const QueryString: QueryStringClass; - -export { QueryString }; +export const url = { + encodeQuery, + encodeUriQuery, +}; diff --git a/src/plugins/kibana_utils/public/history/remove_query_param.ts b/src/plugins/kibana_utils/public/history/remove_query_param.ts index fbf985998b4cd..bf945e5b064aa 100644 --- a/src/plugins/kibana_utils/public/history/remove_query_param.ts +++ b/src/plugins/kibana_utils/public/history/remove_query_param.ts @@ -17,16 +17,18 @@ * under the License. */ +import { parse, stringify } from 'query-string'; import { History, Location } from 'history'; -import { parse } from 'querystring'; -import { stringifyQueryString } from '../state_management/url/stringify_query_string'; // TODO: extract it to ../url +import { url } from '../../common'; export function removeQueryParam(history: History, param: string, replace: boolean = true) { const oldLocation = history.location; const search = (oldLocation.search || '').replace(/^\?/, ''); - const query = parse(search); + const query = parse(search, { sort: false }); + delete query[param]; - const newSearch = stringifyQueryString(query); + + const newSearch = stringify(url.encodeQuery(query), { sort: false, encode: false }); const newLocation: Location = { ...oldLocation, search: newSearch, diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 6a285de12135b..6971d96e471bd 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -26,6 +26,7 @@ export { Set, UiComponent, UiComponentInstance, + url, JsonValue, JsonObject, JsonArray, diff --git a/src/plugins/kibana_utils/public/state_management/url/format.ts b/src/plugins/kibana_utils/public/state_management/url/format.ts index 988ee08627382..2912b665ff014 100644 --- a/src/plugins/kibana_utils/public/state_management/url/format.ts +++ b/src/plugins/kibana_utils/public/state_management/url/format.ts @@ -18,18 +18,22 @@ */ import { format as formatUrl } from 'url'; -import { ParsedUrlQuery } from 'querystring'; +import { stringify, ParsedQuery } from 'query-string'; import { parseUrl, parseUrlHash } from './parse'; -import { stringifyQueryString } from './stringify_query_string'; +import { url as urlUtils } from '../../../common'; export function replaceUrlHashQuery( rawUrl: string, - queryReplacer: (query: ParsedUrlQuery) => ParsedUrlQuery + queryReplacer: (query: ParsedQuery) => ParsedQuery ) { const url = parseUrl(rawUrl); const hash = parseUrlHash(rawUrl); const newQuery = queryReplacer(hash?.query || {}); - const searchQueryString = stringifyQueryString(newQuery); + const searchQueryString = stringify(urlUtils.encodeQuery(newQuery), { + sort: false, + encode: false, + }); + if ((!hash || !hash.search) && !searchQueryString) return rawUrl; // nothing to change. return original url return formatUrl({ ...url, diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts index 1dd204e717213..40a411d425a54 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts @@ -18,11 +18,12 @@ */ import { format as formatUrl } from 'url'; +import { stringify } from 'query-string'; import { createBrowserHistory, History } from 'history'; import { decodeState, encodeState } from '../state_encoder'; import { getCurrentUrl, parseUrl, parseUrlHash } from './parse'; -import { stringifyQueryString } from './stringify_query_string'; import { replaceUrlHashQuery } from './format'; +import { url as urlUtils } from '../../../common'; /** * Parses a kibana url and retrieves all the states encoded into url, @@ -243,11 +244,11 @@ export function getRelativeToHistoryPath(absoluteUrl: string, history: History): return formatUrl({ pathname: stripBasename(parsedUrl.pathname), - search: stringifyQueryString(parsedUrl.query), + search: stringify(urlUtils.encodeQuery(parsedUrl.query), { sort: false, encode: false }), hash: parsedHash ? formatUrl({ pathname: parsedHash.pathname, - search: stringifyQueryString(parsedHash.query), + search: stringify(urlUtils.encodeQuery(parsedHash.query), { sort: false, encode: false }), }) : parsedUrl.hash, }); diff --git a/src/plugins/kibana_utils/public/state_management/url/stringify_query_string.ts b/src/plugins/kibana_utils/public/state_management/url/stringify_query_string.ts deleted file mode 100644 index e951dfac29c02..0000000000000 --- a/src/plugins/kibana_utils/public/state_management/url/stringify_query_string.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. 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 { stringify, ParsedUrlQuery } from 'querystring'; - -// encodeUriQuery implements the less-aggressive encoding done naturally by -// the browser. We use it to generate the same urls the browser would -export const stringifyQueryString = (query: ParsedUrlQuery) => - stringify(query, undefined, undefined, { - // encode spaces with %20 is needed to produce the same queries as angular does - // https://github.com/angular/angular.js/blob/51c516e7d4f2d10b0aaa4487bd0b52772022207a/src/Angular.js#L1377 - encodeURIComponent: (val: string) => encodeUriQuery(val, true), - }); - -/** - * Extracted from angular.js - * repo: https://github.com/angular/angular.js - * license: MIT - https://github.com/angular/angular.js/blob/51c516e7d4f2d10b0aaa4487bd0b52772022207a/LICENSE - * source: https://github.com/angular/angular.js/blob/51c516e7d4f2d10b0aaa4487bd0b52772022207a/src/Angular.js#L1413-L1432 - */ - -/** - * This method is intended for encoding *key* or *value* parts of query component. We need a custom - * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be - * encoded per http://tools.ietf.org/html/rfc3986: - * query = *( pchar / "/" / "?" ) - * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" - * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - * pct-encoded = "%" HEXDIG HEXDIG - * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" - * / "*" / "+" / "," / ";" / "=" - */ -export function encodeUriQuery(val: string, pctEncodeSpaces: boolean = false) { - return encodeURIComponent(val) - .replace(/%40/gi, '@') - .replace(/%3A/gi, ':') - .replace(/%24/g, '$') - .replace(/%2C/gi, ',') - .replace(/%3B/gi, ';') - .replace(/%20/g, pctEncodeSpaces ? '%20' : '+'); -} diff --git a/src/plugins/kibana_utils/server/index.ts b/src/plugins/kibana_utils/server/index.ts index f8b79a1b8b339..b8b768da0192e 100644 --- a/src/plugins/kibana_utils/server/index.ts +++ b/src/plugins/kibana_utils/server/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { Get, Set, createGetterSetter } from '../common'; +export { Get, Set, createGetterSetter, url } from '../common'; diff --git a/src/plugins/timelion/server/series_functions/quandl.test.js b/src/plugins/timelion/server/series_functions/quandl.test.js index fe5aab512370f..67d81e56f145f 100644 --- a/src/plugins/timelion/server/series_functions/quandl.test.js +++ b/src/plugins/timelion/server/series_functions/quandl.test.js @@ -17,16 +17,16 @@ * under the License. */ +import { parse } from 'query-string'; import fn from './quandl'; +import moment from 'moment'; +import fetchMock from 'node-fetch'; const parseURL = require('url').parse; -const parseQueryString = require('querystring').parse; const tlConfig = require('./fixtures/tl_config')(); -import moment from 'moment'; -import fetchMock from 'node-fetch'; function parseUrlParams(url) { - return parseQueryString(parseURL(url).query); + return parse(parseURL(url).query, { sort: false }); } jest.mock('node-fetch', () => diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.test.tsx index ac728e72fa877..286af610707e1 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.test.tsx @@ -16,6 +16,44 @@ describe('toQuery', () => { }); describe('fromQuery', () => { + it('should not encode the following characters', () => { + expect( + fromQuery({ + a: true, + b: 5000, + c: ':' + }) + ).toEqual('a=true&b=5000&c=:'); + }); + + it('should encode the following characters', () => { + expect( + fromQuery({ + a: '@', + b: '.', + c: ';', + d: ' ' + }) + ).toEqual('a=%40&b=.&c=%3B&d=%20'); + }); + + it('should handle null and undefined', () => { + expect( + fromQuery({ + a: undefined, + b: null + }) + ).toEqual('a=&b='); + }); + + it('should handle arrays', () => { + expect( + fromQuery({ + arr: ['a', 'b'] + }) + ).toEqual('arr=a%2Cb'); + }); + it('should parse object to string', () => { expect( fromQuery({ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts index 357ea23d522a0..36465309b736e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts @@ -4,19 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import qs from 'querystring'; +import { parse, stringify } from 'query-string'; import { LocalUIFilterName } from '../../../../server/lib/ui_filters/local_ui_filters/config'; +import { url } from '../../../../../../../../src/plugins/kibana_utils/public'; export function toQuery(search?: string): APMQueryParamsRaw { - return search ? qs.parse(search.slice(1)) : {}; + return search ? parse(search.slice(1), { sort: false }) : {}; } export function fromQuery(query: Record) { - return qs.stringify(query, undefined, undefined, { - encodeURIComponent: (value: string) => { - return encodeURIComponent(value).replace(/%3A/g, ':'); - } - }); + const encodedQuery = url.encodeQuery(query, value => + encodeURIComponent(value).replace(/%3A/g, ':') + ); + + return stringify(encodedQuery, { sort: false, encode: false }); } export type APMQueryParams = { diff --git a/x-pack/legacy/plugins/beats_management/public/containers/with_url_state.tsx b/x-pack/legacy/plugins/beats_management/public/containers/with_url_state.tsx index 71e9163fe22e7..c8f756da985a7 100644 --- a/x-pack/legacy/plugins/beats_management/public/containers/with_url_state.tsx +++ b/x-pack/legacy/plugins/beats_management/public/containers/with_url_state.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse, stringify } from 'querystring'; +import { parse, stringify } from 'query-string'; import React from 'react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { FlatObject } from '../frontend_types'; @@ -31,7 +31,9 @@ export class WithURLStateComponent extends React.Compon > { private get URLState(): URLState { // slice because parse does not account for the initial ? in the search string - return parse(decodeURIComponent(this.props.history.location.search).substring(1)) as URLState; + return parse(decodeURIComponent(this.props.history.location.search).substring(1), { + sort: false, + }) as URLState; } private historyListener: (() => void) | null = null; @@ -63,10 +65,13 @@ export class WithURLStateComponent extends React.Compon newState = state; } - const search: string = stringify({ - ...pastState, - ...newState, - }); + const search: string = stringify( + { + ...pastState, + ...newState, + }, + { sort: false } + ); const newLocation = { ...this.props.history.location, diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts index f7f191a48de82..5adbf4ce66c13 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts @@ -7,8 +7,8 @@ import rison from 'rison-node'; // @ts-ignore Untyped local. import { fetch } from '../../../../common/lib/fetch'; -import { getStartPlugins } from '../../../legacy'; import { CanvasWorkpad } from '../../../../types'; +import { url } from '../../../../../../../../src/plugins/kibana_utils/public'; // type of the desired pdf output (print or preserve_layout) const PDF_LAYOUT_TYPE = 'preserve_layout'; @@ -71,11 +71,10 @@ function getPdfUrlParts( export function getPdfUrl(...args: Arguments): string { const urlParts = getPdfUrlParts(...args); + const param = (key: string, val: any) => + url.encodeUriQuery(key, true) + (val === true ? '' : '=' + url.encodeUriQuery(val, true)); - return `${urlParts.createPdfUri}?${getStartPlugins().__LEGACY.QueryString.param( - 'jobParams', - urlParts.createPdfPayload.jobParams - )}`; + return `${urlParts.createPdfUri}?${param('jobParams', urlParts.createPdfPayload.jobParams)}`; } export function createPdf(...args: Arguments) { diff --git a/x-pack/legacy/plugins/canvas/public/legacy.ts b/x-pack/legacy/plugins/canvas/public/legacy.ts index c16bc124747c6..ea873e6f2296d 100644 --- a/x-pack/legacy/plugins/canvas/public/legacy.ts +++ b/x-pack/legacy/plugins/canvas/public/legacy.ts @@ -13,8 +13,6 @@ import { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; // eslint-d import { Storage } from '../../../../../src/plugins/kibana_utils/public'; // eslint-disable-line import/order // @ts-ignore Untyped Kibana Lib import { formatMsg } from '../../../../../src/plugins/kibana_legacy/public'; // eslint-disable-line import/order -// @ts-ignore Untyped Kibana Lib -import { QueryString } from 'ui/utils/query_string'; // eslint-disable-line import/order const shimCoreSetup = { ...npSetup.core, @@ -33,7 +31,6 @@ const shimStartPlugins: CanvasStartDeps = { absoluteToParsedUrl, // ToDo: Copy directly into canvas formatMsg, - QueryString, storage: Storage, // ToDo: Won't be a part of New Platform. Will need to handle internally trackSubUrlForApp: chrome.trackSubUrlForApp, diff --git a/x-pack/legacy/plugins/canvas/public/lib/app_state.ts b/x-pack/legacy/plugins/canvas/public/lib/app_state.ts index c93e505c595fd..d431202ba75a4 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/app_state.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/app_state.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import querystring from 'querystring'; +import { parse } from 'query-string'; import { get } from 'lodash'; // @ts-ignore untyped local import { getInitialState } from '../state/initial_state'; @@ -38,7 +38,7 @@ export function getDefaultAppState(): AppState { export function getCurrentAppState(): AppState { const history = historyProvider(getWindow()); const { search } = history.getLocation(); - const qs = !!search ? querystring.parse(search.replace(/^\?/, '')) : {}; + const qs = !!search ? parse(search.replace(/^\?/, ''), { sort: false }) : {}; const appState = assignAppState({}, qs); return appState; diff --git a/x-pack/legacy/plugins/canvas/public/lib/modify_url.ts b/x-pack/legacy/plugins/canvas/public/lib/modify_url.ts index 890138c41d7bf..d128dc432e9cf 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/modify_url.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/modify_url.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ParsedUrlQuery } from 'querystring'; +import { ParsedQuery } from 'query-string'; import { format as formatUrl, parse as parseUrl, UrlObject } from 'url'; /** @@ -20,7 +20,7 @@ export interface URLMeaningfulParts { protocol?: string | null; slashes?: boolean | null; port?: string | null; - query: ParsedUrlQuery; + query: ParsedQuery; } /** diff --git a/x-pack/legacy/plugins/canvas/public/plugin.tsx b/x-pack/legacy/plugins/canvas/public/plugin.tsx index a24fd758808ba..44731628cf653 100644 --- a/x-pack/legacy/plugins/canvas/public/plugin.tsx +++ b/x-pack/legacy/plugins/canvas/public/plugin.tsx @@ -43,7 +43,6 @@ export interface CanvasStartDeps { __LEGACY: { absoluteToParsedUrl: (url: string, basePath: string) => any; formatMsg: any; - QueryString: any; storage: typeof Storage; trackSubUrlForApp: Chrome['trackSubUrlForApp']; }; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/query_params.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/query_params.js index f0871d62976ed..af462bfeffcf5 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/query_params.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/query_params.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'querystring'; +import { parse } from 'query-string'; export function extractQueryParams(queryString) { const hrefSplit = queryString.split('?'); @@ -12,5 +12,5 @@ export function extractQueryParams(queryString) { return {}; } - return parse(hrefSplit[1]); + return parse(hrefSplit[1], { sort: false }); } diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/routing.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/routing.js index bb4b4540b1922..487b1068794f9 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/routing.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/routing.js @@ -9,7 +9,7 @@ */ import { createLocation } from 'history'; -import { stringify } from 'querystring'; +import { stringify } from 'query-string'; import { APPS, BASE_PATH, BASE_PATH_REMOTE_CLUSTERS } from '../../../common/constants'; const isModifiedEvent = event => @@ -22,16 +22,7 @@ const queryParamsFromObject = (params, encodeParams = false) => { return; } - const paramsStr = stringify( - params, - '&', - '=', - encodeParams - ? {} - : { - encodeURIComponent: val => val, // Don't encode special chars - } - ); + const paramsStr = stringify(params, { sort: false, encode: encodeParams }); return `?${paramsStr}`; }; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx index 536dd24faa7c1..9fbba94407dc0 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx @@ -8,10 +8,11 @@ import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { encode } from 'rison-node'; -import { QueryString } from 'ui/utils/query_string'; import url from 'url'; +import { stringify } from 'query-string'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { TimeRange } from '../../../../common/http_api/shared/time_range'; +import { url as urlUtils } from '../../../../../../../../src/plugins/kibana_utils/public'; export const AnalyzeInMlButton: React.FunctionComponent<{ jobId: string; @@ -61,7 +62,7 @@ const getOverallAnomalyExplorerLink = (pathname: string, jobId: string, timeRang }, }); - const hash = `/explorer?${QueryString.encode({ _g })}`; + const hash = `/explorer?${stringify(urlUtils.encodeQuery({ _g }), { encode: false })}`; return url.format({ pathname, @@ -94,7 +95,10 @@ const getPartitionSpecificSingleMetricViewerLink = ( }, }); - const hash = `/timeseriesexplorer?${QueryString.encode({ _g, _a })}`; + const hash = `/timeseriesexplorer?${stringify(urlUtils.encodeQuery({ _g, _a }), { + sort: false, + encode: false, + })}`; return url.format({ pathname, diff --git a/x-pack/legacy/plugins/infra/public/containers/with_state_from_location.tsx b/x-pack/legacy/plugins/infra/public/containers/with_state_from_location.tsx index ec6345c49c303..6f7baf6b98b62 100644 --- a/x-pack/legacy/plugins/infra/public/containers/with_state_from_location.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/with_state_from_location.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse, stringify } from 'query-string'; import { Location } from 'history'; import omit from 'lodash/fp/omit'; -import { parse as parseQueryString, stringify as stringifyQueryString } from 'querystring'; import React from 'react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; // eslint-disable-next-line @typescript-eslint/camelcase @@ -102,7 +102,7 @@ const encodeRisonAppState = (state: AnyObject) => ({ export const mapRisonAppLocationToState = ( mapState: (risonAppState: AnyObject) => State = (state: AnyObject) => state as State ) => (location: Location): State => { - const queryValues = parseQueryString(location.search.substring(1)); + const queryValues = parse(location.search.substring(1), { sort: false }); const decodedState = decodeRisonAppState(queryValues); return mapState(decodedState); }; @@ -110,17 +110,20 @@ export const mapRisonAppLocationToState = ( export const mapStateToRisonAppLocation = ( mapState: (state: State) => AnyObject = (state: State) => state ) => (state: State, location: Location): Location => { - const previousQueryValues = parseQueryString(location.search.substring(1)); + const previousQueryValues = parse(location.search.substring(1), { sort: false }); const previousState = decodeRisonAppState(previousQueryValues); const encodedState = encodeRisonAppState({ ...previousState, ...mapState(state), }); - const newQueryValues = stringifyQueryString({ - ...previousQueryValues, - ...encodedState, - }); + const newQueryValues = stringify( + { + ...previousQueryValues, + ...encodedState, + }, + { sort: false } + ); return { ...location, search: `?${newQueryValues}`, diff --git a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx index a418be01d1ed2..e9ec053f8c609 100644 --- a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx @@ -19,7 +19,7 @@ describe('RedirectToLogs component', () => { expect(component).toMatchInlineSnapshot(` `); }); @@ -33,7 +33,7 @@ describe('RedirectToLogs component', () => { expect(component).toMatchInlineSnapshot(` `); }); @@ -45,7 +45,7 @@ describe('RedirectToLogs component', () => { expect(component).toMatchInlineSnapshot(` `); }); diff --git a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx index 2d1f3a32988aa..1e97072cac109 100644 --- a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx @@ -35,7 +35,7 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` `); }); @@ -47,7 +47,7 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` `); }); @@ -59,7 +59,7 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` `); }); @@ -73,7 +73,7 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` `); }); @@ -89,7 +89,7 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` `); }); @@ -103,7 +103,7 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` `); }); diff --git a/x-pack/legacy/plugins/infra/public/utils/url_state.tsx b/x-pack/legacy/plugins/infra/public/utils/url_state.tsx index 66bb4308d1d16..58835715fe55c 100644 --- a/x-pack/legacy/plugins/infra/public/utils/url_state.tsx +++ b/x-pack/legacy/plugins/infra/public/utils/url_state.tsx @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse, stringify } from 'query-string'; import { History, Location } from 'history'; import throttle from 'lodash/fp/throttle'; import React from 'react'; import { Route, RouteProps } from 'react-router-dom'; import { decode, encode, RisonValue } from 'rison-node'; - -import { QueryString } from 'ui/utils/query_string'; +import { url } from '../../../../../../src/plugins/kibana_utils/public'; interface UrlStateContainerProps { urlState: UrlState | undefined; @@ -145,7 +145,9 @@ const encodeRisonUrlState = (state: any) => encode(state); export const getQueryStringFromLocation = (location: Location) => location.search.substring(1); export const getParamFromQueryString = (queryString: string, key: string): string | undefined => { - const queryParam = QueryString.decode(queryString)[key]; + const parsedQueryString: Record = parse(queryString, { sort: false }); + const queryParam = parsedQueryString[key]; + return Array.isArray(queryParam) ? queryParam[0] : queryParam; }; @@ -153,13 +155,17 @@ export const replaceStateKeyInQueryString = ( stateKey: string, urlState: UrlState | undefined ) => (queryString: string) => { - const previousQueryValues = QueryString.decode(queryString); + const previousQueryValues = parse(queryString, { sort: false }); const encodedUrlState = typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined; - return QueryString.encode({ - ...previousQueryValues, - [stateKey]: encodedUrlState, - }); + + return stringify( + url.encodeQuery({ + ...previousQueryValues, + [stateKey]: encodedUrlState, + }), + { sort: false, encode: false } + ); }; const replaceQueryStringInLocation = (location: Location, queryString: string): Location => { diff --git a/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts b/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts index 79a5d552bcd78..284af62e52fbb 100644 --- a/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts +++ b/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse, stringify } from 'query-string'; import { Location } from 'history'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { decode, encode, RisonValue } from 'rison-node'; -import { QueryString } from 'ui/utils/query_string'; +import { url } from '../../../../../../src/plugins/kibana_utils/public'; import { useHistory } from './history_context'; @@ -84,7 +85,7 @@ export const useUrlState = ({ return [state, setState] as [typeof state, typeof setState]; }; -const decodeRisonUrlState = (value: string | undefined): RisonValue | undefined => { +const decodeRisonUrlState = (value: string | undefined | null): RisonValue | undefined => { try { return value ? decode(value) : undefined; } catch (error) { @@ -99,8 +100,10 @@ const encodeRisonUrlState = (state: any) => encode(state); const getQueryStringFromLocation = (location: Location) => location.search.substring(1); -const getParamFromQueryString = (queryString: string, key: string): string | undefined => { - const queryParam = QueryString.decode(queryString)[key]; +const getParamFromQueryString = (queryString: string, key: string) => { + const parsedQueryString = parse(queryString, { sort: false }); + const queryParam = parsedQueryString[key]; + return Array.isArray(queryParam) ? queryParam[0] : queryParam; }; @@ -108,13 +111,17 @@ export const replaceStateKeyInQueryString = ( stateKey: string, urlState: UrlState | undefined ) => (queryString: string) => { - const previousQueryValues = QueryString.decode(queryString); + const previousQueryValues = parse(queryString, { sort: false }); const encodedUrlState = typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined; - return QueryString.encode({ - ...previousQueryValues, - [stateKey]: encodedUrlState, - }); + + return stringify( + url.encodeQuery({ + ...previousQueryValues, + [stateKey]: encodedUrlState, + }), + { sort: false, encode: false } + ); }; const replaceQueryStringInLocation = (location: Location, queryString: string): Location => { diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx index 3ca23998d5b75..e00ff0333bb73 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -4,12 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse } from 'query-string'; import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { decode } from 'rison-node'; - -// @ts-ignore -import queryString from 'query-string'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; @@ -36,7 +34,8 @@ export const analyticsJobExplorationRoute: MlRoute = { const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); - const { _g } = queryString.parse(location.search); + const { _g }: Record = parse(location.search, { sort: false }); + let globalState: any = null; try { globalState = decode(_g); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx index fa4745f19e3b4..74ab916cb443f 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse } from 'query-string'; import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; - -// @ts-ignore -import queryString from 'query-string'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { Page } from '../../../datavisualizer/index_based'; @@ -37,7 +35,7 @@ export const indexBasedRoute: MlRoute = { }; const PageWrapper: FC = ({ location, deps }) => { - const { index, savedSearchId } = queryString.parse(location.search); + const { index, savedSearchId }: Record = parse(location.search, { sort: false }); const { context } = useResolver(index, savedSearchId, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx index c2e87f065116e..f0a25d880a082 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse } from 'query-string'; import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; - -// @ts-ignore -import queryString from 'query-string'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; @@ -33,7 +31,7 @@ export const jobTypeRoute: MlRoute = { }; const PageWrapper: FC = ({ location, deps }) => { - const { index, savedSearchId } = queryString.parse(location.search); + const { index, savedSearchId }: Record = parse(location.search, { sort: false }); const { context } = useResolver(index, savedSearchId, deps.config, basicResolvers(deps)); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx index 78f72a7b7a39b..12687fd71edc5 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse } from 'query-string'; import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -// @ts-ignore -import queryString from 'query-string'; - import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; @@ -41,7 +39,7 @@ export const checkViewOrCreateRoute: MlRoute = { }; const PageWrapper: FC = ({ location, deps }) => { - const { id, index, savedSearchId } = queryString.parse(location.search); + const { id, index, savedSearchId }: Record = parse(location.search, { sort: false }); const { context, results } = useResolver(index, savedSearchId, deps.config, { ...basicResolvers(deps), existingJobsAndGroups: mlJobService.getJobAndGroupIds, @@ -55,7 +53,10 @@ const PageWrapper: FC = ({ location, deps }) => { }; const CheckViewOrCreateWrapper: FC = ({ location, deps }) => { - const { id: moduleId, index: indexPatternId } = queryString.parse(location.search); + const { id: moduleId, index: indexPatternId }: Record = parse(location.search, { + sort: false, + }); + // the single resolver checkViewOrCreateJobs redirects only. so will always reject useResolver(undefined, undefined, deps.config, { checkViewOrCreateJobs: () => checkViewOrCreateJobs(moduleId, indexPatternId), diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx index 230d96456427c..b1256e21888d9 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse } from 'query-string'; import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -// @ts-ignore -import queryString from 'query-string'; import { basicResolvers } from '../../resolvers'; import { MlRoute, PageLoader, PageProps } from '../../router'; @@ -113,7 +112,7 @@ export const categorizationRoute: MlRoute = { }; const PageWrapper: FC = ({ location, jobType, deps }) => { - const { index, savedSearchId } = queryString.parse(location.search); + const { index, savedSearchId }: Record = parse(location.search, { sort: false }); const { context, results } = useResolver(index, savedSearchId, deps.config, { ...basicResolvers(deps), privileges: checkCreateJobsPrivilege, diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index 2bf3d50c3678c..5bc2435db078c 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -8,8 +8,6 @@ import { isEqual } from 'lodash'; import React, { FC, useCallback, useEffect, useState } from 'react'; import { usePrevious } from 'react-use'; import moment from 'moment'; -// @ts-ignore -import queryString from 'query-string'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/ml/public/application/util/url_state.ts b/x-pack/legacy/plugins/ml/public/application/util/url_state.ts index e7d5a94e2694f..b0699116895d4 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/url_state.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/url_state.ts @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse, stringify } from 'query-string'; import { useCallback } from 'react'; import { isEqual } from 'lodash'; -// @ts-ignore -import queryString from 'query-string'; import { decode, encode } from 'rison-node'; import { useHistory, useLocation } from 'react-router-dom'; @@ -33,12 +32,12 @@ function isRisonSerializationRequired(queryParam: string): boolean { export function getUrlState(search: string): Dictionary { const urlState: Dictionary = {}; - const parsedQueryString = queryString.parse(search); + const parsedQueryString = parse(search, { sort: false }); try { Object.keys(parsedQueryString).forEach(a => { if (isRisonSerializationRequired(a)) { - urlState[a] = decode(parsedQueryString[a]) as Dictionary; + urlState[a] = decode(parsedQueryString[a] as string); } else { urlState[a] = parsedQueryString[a]; } @@ -64,7 +63,7 @@ export const useUrlState = (accessor: string): UrlState => { const setUrlState = useCallback( (attribute: string | Dictionary, value?: any) => { const urlState = getUrlState(search); - const parsedQueryString = queryString.parse(search); + const parsedQueryString = parse(search, { sort: false }); if (!Object.prototype.hasOwnProperty.call(urlState, accessor)) { urlState[accessor] = {}; @@ -84,7 +83,7 @@ export const useUrlState = (accessor: string): UrlState => { } try { - const oldLocationSearch = queryString.stringify(parsedQueryString, { encode: false }); + const oldLocationSearch = stringify(parsedQueryString, { sort: false, encode: false }); Object.keys(urlState).forEach(a => { if (isRisonSerializationRequired(a)) { @@ -93,11 +92,11 @@ export const useUrlState = (accessor: string): UrlState => { parsedQueryString[a] = urlState[a]; } }); - const newLocationSearch = queryString.stringify(parsedQueryString, { encode: false }); + const newLocationSearch = stringify(parsedQueryString, { sort: false, encode: false }); if (oldLocationSearch !== newLocationSearch) { history.push({ - search: queryString.stringify(parsedQueryString), + search: stringify(parsedQueryString, { sort: false }), }); } } catch (error) { diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/services/query_params.js b/x-pack/legacy/plugins/remote_clusters/public/app/services/query_params.js index f0871d62976ed..af462bfeffcf5 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/services/query_params.js +++ b/x-pack/legacy/plugins/remote_clusters/public/app/services/query_params.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'querystring'; +import { parse } from 'query-string'; export function extractQueryParams(queryString) { const hrefSplit = queryString.split('?'); @@ -12,5 +12,5 @@ export function extractQueryParams(queryString) { return {}; } - return parse(hrefSplit[1]); + return parse(hrefSplit[1], { sort: false }); } diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/encode_uri_query.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/encode_uri_query.js deleted file mode 100644 index ce2346b0f28dc..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/encode_uri_query.js +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-disable @kbn/eslint/require-license-header */ - -// This function was extracted from angular v1.3 - -/* @notice - * This product includes code that was extracted from angular@1.3. - * Original license: - * The MIT License - * - * Copyright (c) 2010-2014 Google, Inc. http://angularjs.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -export function encodeUriQuery(val, pctEncodeSpaces) { - return encodeURIComponent(val) - .replace(/%40/gi, '@') - .replace(/%3A/gi, ':') - .replace(/%24/g, '$') - .replace(/%2C/gi, ',') - .replace(/%3B/gi, ';') - .replace(/%20/g, pctEncodeSpaces ? '%20' : '+'); -} diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/uri_encode.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/uri_encode.js index 5b93461bfaffb..f764271c22a2d 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/uri_encode.js +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/uri_encode.js @@ -5,20 +5,20 @@ */ import { forEach, isArray } from 'lodash'; -import { encodeUriQuery } from './encode_uri_query'; +import { url } from '../../../../../../../../src/plugins/kibana_utils/server'; function toKeyValue(obj) { const parts = []; forEach(obj, function(value, key) { if (isArray(value)) { forEach(value, function(arrayValue) { - const keyStr = encodeUriQuery(key, true); - const valStr = arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true); + const keyStr = url.encodeUriQuery(key, true); + const valStr = arrayValue === true ? '' : '=' + url.encodeUriQuery(arrayValue, true); parts.push(keyStr + valStr); }); } else { - const keyStr = encodeUriQuery(key, true); - const valStr = value === true ? '' : '=' + encodeUriQuery(value, true); + const keyStr = url.encodeUriQuery(key, true); + const valStr = value === true ? '' : '=' + url.encodeUriQuery(value, true); parts.push(keyStr + valStr); } }); @@ -27,5 +27,5 @@ function toKeyValue(obj) { export const uriEncode = { stringify: toKeyValue, - string: encodeUriQuery, + string: url.encodeUriQuery, }; diff --git a/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts b/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts index 9056c7967b4a8..d471dc57fc9e1 100644 --- a/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts +++ b/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts @@ -3,16 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { stringify } from 'query-string'; import { npStart } from 'ui/new_platform'; -import querystring from 'querystring'; - -const { core } = npStart; - // @ts-ignore import rison from 'rison-node'; import { add } from './job_completion_notifications'; +const { core } = npStart; const API_BASE_URL = '/api/reporting/generate'; interface JobParams { @@ -20,7 +17,7 @@ interface JobParams { } export const getReportingJobPath = (exportType: string, jobParams: JobParams) => { - const params = querystring.stringify({ jobParams: rison.encode(jobParams) }); + const params = stringify({ jobParams: rison.encode(jobParams) }); return `${core.http.basePath.prepend(API_BASE_URL)}/${exportType}?${params}`; }; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx b/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx index 8bd97304a7e21..b5aacdf664c67 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx @@ -4,16 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse, stringify } from 'query-string'; import React from 'react'; import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; -import { QueryString } from 'ui/utils/query_string'; import { addEntitiesToKql } from './add_entities_to_kql'; import { replaceKQLParts } from './replace_kql_parts'; import { emptyEntity, multipleEntities, getMultipleEntities } from './entity_helpers'; import { SiemPageName } from '../../../pages/home/types'; import { HostsTableType } from '../../../store/hosts/model'; +import { url as urlUtils } from '../../../../../../../../src/plugins/kibana_utils/public'; + interface QueryStringType { '?_g': string; query: string | null; @@ -29,13 +31,17 @@ export const MlHostConditionalContainer = React.memo(({ exact path={url} render={({ location }) => { - const queryStringDecoded: QueryStringType = QueryString.decode( - location.search.substring(1) - ); + const queryStringDecoded = parse(location.search.substring(1), { + sort: false, + }) as Required; + if (queryStringDecoded.query != null) { queryStringDecoded.query = replaceKQLParts(queryStringDecoded.query); } - const reEncoded = QueryString.encode(queryStringDecoded); + const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { + sort: false, + encode: false, + }); return ; }} /> @@ -47,14 +53,19 @@ export const MlHostConditionalContainer = React.memo(({ params: { hostName }, }, }) => { - const queryStringDecoded: QueryStringType = QueryString.decode( - location.search.substring(1) - ); + const queryStringDecoded = parse(location.search.substring(1), { + sort: false, + }) as Required; + if (queryStringDecoded.query != null) { queryStringDecoded.query = replaceKQLParts(queryStringDecoded.query); } if (emptyEntity(hostName)) { - const reEncoded = QueryString.encode(queryStringDecoded); + const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { + sort: false, + encode: false, + }); + return ( ); @@ -65,12 +76,20 @@ export const MlHostConditionalContainer = React.memo(({ hosts, queryStringDecoded.query || '' ); - const reEncoded = QueryString.encode(queryStringDecoded); + const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { + sort: false, + encode: false, + }); + return ( ); } else { - const reEncoded = QueryString.encode(queryStringDecoded); + const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { + sort: false, + encode: false, + }); + return ( { - const queryStringDecoded: QueryStringType = QueryString.decode( - location.search.substring(1) - ); + const queryStringDecoded = parse(location.search.substring(1), { + sort: false, + }) as Required; + if (queryStringDecoded.query != null) { queryStringDecoded.query = replaceKQLParts(queryStringDecoded.query); } - const reEncoded = QueryString.encode(queryStringDecoded); + + const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { + sort: false, + encode: false, + }); + return ; }} /> @@ -46,14 +54,20 @@ export const MlNetworkConditionalContainer = React.memo { - const queryStringDecoded: QueryStringType = QueryString.decode( - location.search.substring(1) - ); + const queryStringDecoded = parse(location.search.substring(1), { + sort: false, + }) as Required; + if (queryStringDecoded.query != null) { queryStringDecoded.query = replaceKQLParts(queryStringDecoded.query); } + if (emptyEntity(ip)) { - const reEncoded = QueryString.encode(queryStringDecoded); + const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { + sort: false, + encode: false, + }); + return ; } else if (multipleEntities(ip)) { const ips: string[] = getMultipleEntities(ip); @@ -62,10 +76,16 @@ export const MlNetworkConditionalContainer = React.memo; } else { - const reEncoded = QueryString.encode(queryStringDecoded); + const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { + sort: false, + encode: false, + }); return ; } }} diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts index 34f1ea156eee7..7be775ef0c0e4 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse, stringify } from 'query-string'; import { decode, encode } from 'rison-node'; import * as H from 'history'; -import { QueryString } from 'ui/utils/query_string'; import { Query, Filter } from 'src/plugins/data/public'; import { isEmpty } from 'lodash/fp'; @@ -24,6 +24,8 @@ import { UpdateUrlStateString, } from './types'; +import { url } from '../../../../../../../src/plugins/kibana_utils/public'; + export const decodeRisonUrlState = (value: string | undefined): T | null => { try { return value ? ((decode(value) as unknown) as T) : null; @@ -40,30 +42,35 @@ export const encodeRisonUrlState = (state: any) => encode(state); export const getQueryStringFromLocation = (search: string) => search.substring(1); -export const getParamFromQueryString = (queryString: string, key: string): string | undefined => { - const queryParam = QueryString.decode(queryString)[key]; +export const getParamFromQueryString = (queryString: string, key: string) => { + const parsedQueryString = parse(queryString, { sort: false }); + const queryParam = parsedQueryString[key]; + return Array.isArray(queryParam) ? queryParam[0] : queryParam; }; export const replaceStateKeyInQueryString = (stateKey: string, urlState: T) => ( queryString: string ): string => { - const previousQueryValues = QueryString.decode(queryString); + const previousQueryValues = parse(queryString, { sort: false }); if (urlState == null || (typeof urlState === 'string' && urlState === '')) { delete previousQueryValues[stateKey]; - return QueryString.encode({ - ...previousQueryValues, - }); + + return stringify(url.encodeQuery(previousQueryValues), { sort: false, encode: false }); } // ಠ_ಠ Code was copied from x-pack/legacy/plugins/infra/public/utils/url_state.tsx ಠ_ಠ // Remove this if these utilities are promoted to kibana core const encodedUrlState = typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined; - return QueryString.encode({ - ...previousQueryValues, - [stateKey]: encodedUrlState, - }); + + return stringify( + url.encodeQuery({ + ...previousQueryValues, + [stateKey]: encodedUrlState, + }), + { sort: false, encode: false } + ); }; export const replaceQueryStringInLocation = ( diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx index 6995bc8bf1d40..4adc17b32e189 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx @@ -147,7 +147,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: '/network', search: - '?timeline=(id:hello_timeline_id,isOpen:!t)&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + '?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))&timeline=(id:hello_timeline_id,isOpen:!t)', state: '', }); }); diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx index 642a12411e6f3..8192fe4e026af 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse } from 'query-string'; import React, { Fragment, useState, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { parse } from 'querystring'; import { EuiButton, EuiCallOut, EuiLink, EuiEmptyPrompt, EuiSpacer, EuiIcon } from '@elastic/eui'; import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common/constants'; @@ -86,7 +86,7 @@ export const SnapshotList: React.FunctionComponent(undefined); useEffect(() => { if (search) { - const parsedParams = parse(search.replace(/^\?/, '')); + const parsedParams = parse(search.replace(/^\?/, ''), { sort: false }); const { repository, policy } = parsedParams; if (policy && policy !== filteredPolicy) { diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/repository_add/repository_add.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/repository_add/repository_add.tsx index b4a76ff4329cf..a12ecb4baef5d 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/repository_add/repository_add.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/repository_add/repository_add.tsx @@ -3,9 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import { parse } from 'query-string'; import React, { useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { parse } from 'querystring'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; import { Repository, EmptyRepository } from '../../../../common/types'; @@ -44,7 +45,8 @@ export const RepositoryAdd: React.FunctionComponent = ({ if (error) { setSaveError(error); } else { - const { redirect } = parse(search.replace(/^\?/, '')); + const { redirect } = parse(search.replace(/^\?/, ''), { sort: false }); + history.push(redirect ? (redirect as string) : `${BASE_PATH}/${section}/${name}`); } }; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts b/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts index e509e14223006..dc309943d7cf9 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts +++ b/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import qs from 'querystring'; +import { parse, stringify } from 'query-string'; import { useLocation, useHistory } from 'react-router-dom'; import { UptimeUrlParams, getSupportedUrlParams } from '../lib/helper'; @@ -23,14 +23,17 @@ export const useUrlParams: UptimeUrlParamsHook = () => { search = location.search; } - const params = search ? { ...qs.parse(search[0] === '?' ? search.slice(1) : search) } : {}; + const params = search + ? parse(search[0] === '?' ? search.slice(1) : search, { sort: false }) + : {}; + return getSupportedUrlParams(params); }; const updateUrlParams: UpdateUrlParams = updatedParams => { if (!history || !location) return; const { pathname, search } = location; - const currentParams: any = qs.parse(search[0] === '?' ? search.slice(1) : search); + const currentParams = parse(search[0] === '?' ? search.slice(1) : search, { sort: false }); const mergedParams = { ...currentParams, ...updatedParams, @@ -38,7 +41,7 @@ export const useUrlParams: UptimeUrlParamsHook = () => { history.push({ pathname, - search: qs.stringify( + search: stringify( // drop any parameters that have no value Object.keys(mergedParams).reduce((params, key) => { const value = mergedParams[key]; @@ -49,7 +52,8 @@ export const useUrlParams: UptimeUrlParamsHook = () => { ...params, [key]: value, }; - }, {}) + }, {}), + { sort: false } ), }); }; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/stringify_url_params.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/stringify_url_params.ts index 7d00a27d69032..a8ce86c4399e2 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/stringify_url_params.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/stringify_url_params.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import qs from 'querystring'; +import { stringify } from 'query-string'; import { UptimeUrlParams } from './url_params'; import { CLIENT_DEFAULTS } from '../../../common/constants'; @@ -38,5 +38,5 @@ export const stringifyUrlParams = (params: Partial, ignoreEmpty } }); } - return `?${qs.stringify(params)}`; + return `?${stringify(params, { sort: false })}`; }; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts index f01448d9e37ac..11dfc3f21b1bf 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts @@ -46,7 +46,7 @@ const { * require further development. */ export const getSupportedUrlParams = (params: { - [key: string]: string | string[] | undefined; + [key: string]: string | string[] | undefined | null; }): UptimeUrlParams => { const filteredParams: { [key: string]: string | undefined } = {}; Object.keys(params).forEach(key => { diff --git a/x-pack/legacy/plugins/uptime/public/state/api/ping.ts b/x-pack/legacy/plugins/uptime/public/state/api/ping.ts index e0c358fe40e71..c61bf42c8c90e 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/ping.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/ping.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import qs from 'querystring'; +import { stringify } from 'query-string'; import { getApiPath } from '../../lib/helper'; import { APIFn } from './types'; import { GetPingHistogramParams, HistogramResult } from '../../../common/types'; @@ -25,7 +25,7 @@ export const fetchPingHistogram: APIFn ...(statusFilter && { statusFilter }), ...(filters && { filters }), }; - const urlParams = qs.stringify(params).toString(); + const urlParams = stringify(params, { sort: false }); const response = await fetch(`${url}?${urlParams}`); if (!response.ok) { throw new Error(response.statusText); diff --git a/x-pack/package.json b/x-pack/package.json index 921f6ad991188..c1225f609ebbb 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -292,7 +292,9 @@ "proper-lockfile": "^3.2.0", "puid": "1.0.7", "puppeteer-core": "^1.19.0", + "query-string": "6.10.1", "raw-loader": "3.1.0", + "re-resizable": "^6.1.1", "react": "^16.12.0", "react-apollo": "^2.1.4", "react-beautiful-dnd": "^8.0.7", @@ -324,7 +326,6 @@ "request": "^2.88.0", "reselect": "3.0.1", "resize-observer-polyfill": "^1.5.0", - "re-resizable": "^6.1.1", "rison-node": "0.3.1", "rxjs": "^6.5.3", "semver": "5.7.0", diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts index 11bac195653c6..4a7fac147852b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import qs from 'querystring'; +import { parse } from 'query-string'; import { HttpFetchQuery } from 'src/core/public'; import { AppAction } from '../action'; import { MiddlewareFactory, AlertListData } from '../../types'; export const alertMiddlewareFactory: MiddlewareFactory = coreStart => { - const qp = qs.parse(window.location.search.slice(1)); + const qp = parse(window.location.search.slice(1), { sort: false }); return api => next => async (action: AppAction) => { next(action); diff --git a/x-pack/plugins/spaces/server/lib/utils/url.ts b/x-pack/plugins/spaces/server/lib/utils/url.ts index a5797c0f87868..c91934bb99f1f 100644 --- a/x-pack/plugins/spaces/server/lib/utils/url.ts +++ b/x-pack/plugins/spaces/server/lib/utils/url.ts @@ -8,7 +8,7 @@ // DIRECT COPY FROM `src/core/utils/url`, since it's not possible to import from there, // nor can I re-export from `src/core/server`... -import { ParsedUrlQuery } from 'querystring'; +import { ParsedQuery } from 'query-string'; import { format as formatUrl, parse as parseUrl, UrlObject } from 'url'; export interface URLMeaningfulParts { @@ -19,7 +19,7 @@ export interface URLMeaningfulParts { protocol: string | null; slashes: boolean | null; port: string | null; - query: ParsedUrlQuery | {}; + query: ParsedQuery | {}; } /** diff --git a/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js b/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js index be6139ed7a0a7..e1a435e000fae 100644 --- a/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js +++ b/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js @@ -5,8 +5,7 @@ */ import expect from '@kbn/expect'; -import querystring from 'querystring'; - +import { stringify } from 'query-string'; import { registerHelpers } from './rollup.test_helpers'; import { INDEX_TO_ROLLUP_MAPPINGS, INDEX_PATTERNS_EXTENSION_BASE_PATH } from './constants'; import { getRandomString } from './lib'; @@ -39,7 +38,7 @@ export default function({ getService }) { it('"params" is required', async () => { params = { pattern: 'foo' }; - uri = `${BASE_URI}?${querystring.stringify(params)}`; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; ({ body } = await supertest.get(uri).expect(400)); expect(body.message).to.contain( '[request query.params]: expected value of type [string]' @@ -48,14 +47,14 @@ export default function({ getService }) { it('"params" must be a valid JSON string', async () => { params = { pattern: 'foo', params: 'foobarbaz' }; - uri = `${BASE_URI}?${querystring.stringify(params)}`; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; ({ body } = await supertest.get(uri).expect(400)); expect(body.message).to.contain('[request query.params]: expected JSON string'); }); it('"params" requires a "rollup_index" property', async () => { params = { pattern: 'foo', params: JSON.stringify({}) }; - uri = `${BASE_URI}?${querystring.stringify(params)}`; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; ({ body } = await supertest.get(uri).expect(400)); expect(body.message).to.contain('[request query.params]: "rollup_index" is required'); }); @@ -65,7 +64,7 @@ export default function({ getService }) { pattern: 'foo', params: JSON.stringify({ rollup_index: 'my_index', someProp: 'bar' }), }; - uri = `${BASE_URI}?${querystring.stringify(params)}`; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; ({ body } = await supertest.get(uri).expect(400)); expect(body.message).to.contain('[request query.params]: someProp is not allowed'); }); @@ -76,7 +75,7 @@ export default function({ getService }) { params: JSON.stringify({ rollup_index: 'bar' }), meta_fields: 'stringValue', }; - uri = `${BASE_URI}?${querystring.stringify(params)}`; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; ({ body } = await supertest.get(uri).expect(400)); expect(body.message).to.contain( '[request query.meta_fields]: could not parse array value from [stringValue]' @@ -84,10 +83,13 @@ export default function({ getService }) { }); it('should return 404 the rollup index to query does not exist', async () => { - uri = `${BASE_URI}?${querystring.stringify({ - pattern: 'foo', - params: JSON.stringify({ rollup_index: 'bar' }), - })}`; + uri = `${BASE_URI}?${stringify( + { + pattern: 'foo', + params: JSON.stringify({ rollup_index: 'bar' }), + }, + { sort: false } + )}`; ({ body } = await supertest.get(uri).expect(404)); expect(body.message).to.contain('[index_not_found_exception] no such index [bar]'); }); @@ -105,7 +107,7 @@ export default function({ getService }) { pattern: indexName, params: JSON.stringify({ rollup_index: rollupIndex }), }; - const uri = `${BASE_URI}?${querystring.stringify(params)}`; + const uri = `${BASE_URI}?${stringify(params, { sort: false })}`; const { body } = await supertest.get(uri).expect(200); // Verify that the fields for wildcard correspond to our declared mappings diff --git a/x-pack/test/functional/apps/infra/link_to.ts b/x-pack/test/functional/apps/infra/link_to.ts index 639b65ec5eca8..738dc7efd8fd9 100644 --- a/x-pack/test/functional/apps/infra/link_to.ts +++ b/x-pack/test/functional/apps/infra/link_to.ts @@ -22,7 +22,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { state: undefined, }; const expectedSearchString = - "logFilter=(expression:'trace.id:433b4651687e18be2c6c8e3b11f53d09',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1565707203194),streamLive:!f)&sourceId=default"; + "sourceId=default&logPosition=(position:(tiebreaker:0,time:1565707203194),streamLive:!f)&logFilter=(expression:'trace.id:433b4651687e18be2c6c8e3b11f53d09',kind:kuery)"; const expectedRedirectPath = '/logs/stream?'; await pageObjects.common.navigateToActualUrl( diff --git a/x-pack/test/saml_api_integration/apis/security/saml_login.ts b/x-pack/test/saml_api_integration/apis/security/saml_login.ts index 6ede8aadeb5a7..610850cfb00bb 100644 --- a/x-pack/test/saml_api_integration/apis/security/saml_login.ts +++ b/x-pack/test/saml_api_integration/apis/security/saml_login.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import querystring from 'querystring'; +import { stringify } from 'query-string'; import url from 'url'; import { delay } from 'bluebird'; import expect from '@kbn/expect'; @@ -443,7 +443,7 @@ export default function({ getService }: FtrProviderContext) { it('should invalidate access token on IdP initiated logout', async () => { const logoutRequest = await createLogoutRequest({ sessionIndex: idpSessionIndex }); const logoutResponse = await supertest - .get(`/api/security/logout?${querystring.stringify(logoutRequest)}`) + .get(`/api/security/logout?${stringify(logoutRequest, { sort: false })}`) .set('Cookie', sessionCookie.cookieString()) .expect(302); @@ -479,7 +479,7 @@ export default function({ getService }: FtrProviderContext) { it('should invalidate access token on IdP initiated logout even if there is no Kibana session', async () => { const logoutRequest = await createLogoutRequest({ sessionIndex: idpSessionIndex }); const logoutResponse = await supertest - .get(`/api/security/logout?${querystring.stringify(logoutRequest)}`) + .get(`/api/security/logout?${stringify(logoutRequest, { sort: false })}`) .expect(302); expect(logoutResponse.headers['set-cookie']).to.be(undefined); diff --git a/x-pack/test/saml_api_integration/fixtures/saml_tools.ts b/x-pack/test/saml_api_integration/fixtures/saml_tools.ts index b7b94b8eeb17a..bbe0df7ff3a2c 100644 --- a/x-pack/test/saml_api_integration/fixtures/saml_tools.ts +++ b/x-pack/test/saml_api_integration/fixtures/saml_tools.ts @@ -6,7 +6,7 @@ import crypto from 'crypto'; import fs from 'fs'; -import querystring from 'querystring'; +import { stringify } from 'query-string'; import url from 'url'; import zlib from 'zlib'; import { promisify } from 'util'; @@ -140,7 +140,7 @@ export async function getLogoutRequest({ }; const signer = crypto.createSign('RSA-SHA256'); - signer.update(querystring.stringify(queryStringParameters)); + signer.update(stringify(queryStringParameters, { sort: false })); queryStringParameters.Signature = signer.sign(signingKey.toString(), 'base64'); return queryStringParameters; diff --git a/yarn.lock b/yarn.lock index 491e8ab8cf95d..be4b185b7b77f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23769,6 +23769,15 @@ qs@~6.4.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" integrity sha1-E+JtKK1rD/qpExLNO/cI7TUecjM= +query-string@6.10.1: + version "6.10.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.10.1.tgz#30b3505f6fca741d5ae541964d1b3ae9dc2a0de8" + integrity sha512-SHTUV6gDlgMXg/AQUuLpTiBtW/etZ9JT6k6RCtCyqADquApLX0Aq5oK/s5UeTUAWBG50IExjIr587GqfXRfM4A== + dependencies: + decode-uri-component "^0.2.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + query-string@^4.1.0, query-string@^4.2.2: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" @@ -23786,11 +23795,6 @@ query-string@^5.0.1: object-assign "^4.1.0" strict-uri-encode "^1.0.0" -querystring-browser@1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/querystring-browser/-/querystring-browser-1.0.4.tgz#f2e35881840a819bc7b1bf597faf0979e6622dc6" - integrity sha1-8uNYgYQKgZvHsb9Zf68JeeZiLcY= - querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -27387,6 +27391,11 @@ spdy@^4.0.1: select-hose "^2.0.0" spdy-transport "^3.0.0" +split-on-first@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== + split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" @@ -27700,6 +27709,11 @@ strict-uri-encode@^1.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= + string-length@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac"