From a7550118c0a1c7ed9d316f00493317dbb8b295a5 Mon Sep 17 00:00:00 2001 From: Viktor Tsvetkov <142901247+vtsvetkov-splunk@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:49:20 +0200 Subject: [PATCH] chore(api): encode URL params Signed-off-by: Viktor Tsvetkov <142901247+vtsvetkov-splunk@users.noreply.github.com> --- .../components/BaseFormView/BaseFormView.tsx | 16 ++++--- ui/src/components/ConfigurationFormView.jsx | 4 +- ui/src/components/DeleteModal/DeleteModal.tsx | 10 +++-- .../MultiInputComponent.tsx | 20 +++++---- .../SingleInputComponent.tsx | 29 ++++++------ ui/src/components/table/TableWrapper.tsx | 8 ++-- ui/src/public/mockServiceWorker.js | 2 +- ui/src/types/modules.d.ts | 35 ++++++++++++++- ui/src/util/axiosCallWrapper.ts | 44 +++++++------------ ui/src/util/util.ts | 7 --- 10 files changed, 101 insertions(+), 74 deletions(-) diff --git a/ui/src/components/BaseFormView/BaseFormView.tsx b/ui/src/components/BaseFormView/BaseFormView.tsx index ba71bd584..c347f5311 100644 --- a/ui/src/components/BaseFormView/BaseFormView.tsx +++ b/ui/src/components/BaseFormView/BaseFormView.tsx @@ -9,7 +9,7 @@ import Validator, { SaveValidator } from '../../util/Validator'; import { getUnifiedConfigs, generateToast } from '../../util/util'; import { MODE_CLONE, MODE_CREATE, MODE_EDIT, MODE_CONFIG } from '../../constants/modes'; import { PAGE_INPUT, PAGE_CONF } from '../../constants/pages'; -import { axiosCallWrapper } from '../../util/axiosCallWrapper'; +import { axiosCallWrapper, generateEndPointUrl } from '../../util/axiosCallWrapper'; import { parseErrorMsg, getFormattedMessage } from '../../util/messageUtil'; import { getBuildDirPath } from '../../util/script'; @@ -862,7 +862,7 @@ class BaseFormView extends PureComponent { } axiosCallWrapper({ - serviceName: this.endpoint, + endpointUrl: generateEndPointUrl(encodeURIComponent(this.endpoint)), body, customHeaders: { 'Content-Type': 'application/x-www-form-urlencoded' }, method: 'post', @@ -1134,12 +1134,16 @@ class BaseFormView extends PureComponent { return false; } + const baseUrl = new URL( + `https://${this.datadict.endpoint || this.datadict.endpoint_token}` + ); + baseUrl.pathname = this.oauthConf?.accessTokenEndpoint || ''; + const url = baseUrl.toString(); + const code = decodeURIComponent(message.code); const data: Record = { method: 'POST', - url: `https://${this.datadict.endpoint || this.datadict.endpoint_token}${ - this.oauthConf?.accessTokenEndpoint - }`, + url, grant_type: 'authorization_code', client_id: this.datadict.client_id, client_secret: this.datadict.client_secret, @@ -1159,7 +1163,7 @@ class BaseFormView extends PureComponent { } }); - const OAuthEndpoint = `${this.appName}_oauth/oauth`; + const OAuthEndpoint = `${encodeURIComponent(this.appName)}_oauth/oauth`; // Internal handler call to get the access token and other values axiosCallWrapper({ endpointUrl: OAuthEndpoint, diff --git a/ui/src/components/ConfigurationFormView.jsx b/ui/src/components/ConfigurationFormView.jsx index bdc5060c4..0d86a8f4d 100644 --- a/ui/src/components/ConfigurationFormView.jsx +++ b/ui/src/components/ConfigurationFormView.jsx @@ -8,7 +8,7 @@ import WaitSpinner from '@splunk/react-ui/WaitSpinner'; import axios from 'axios'; import BaseFormView from './BaseFormView/BaseFormView'; import { StyledButton } from '../pages/EntryPageStyle'; -import { axiosCallWrapper } from '../util/axiosCallWrapper'; +import { axiosCallWrapper, generateEndPointUrl } from '../util/axiosCallWrapper'; import { MODE_CONFIG } from '../constants/modes'; import { WaitSpinnerWrapper } from './table/CustomTableStyle'; import { PAGE_CONF } from '../constants/pages'; @@ -28,7 +28,7 @@ function ConfigurationFormView({ serviceName }) { useEffect(() => { const abortController = new AbortController(); axiosCallWrapper({ - serviceName: `settings/${serviceName}`, + endpointUrl: generateEndPointUrl(`settings/${encodeURIComponent(serviceName)}`), handleError: true, signal: abortController.signal, callbackOnError: (err) => { diff --git a/ui/src/components/DeleteModal/DeleteModal.tsx b/ui/src/components/DeleteModal/DeleteModal.tsx index 45852e60b..e7dc8fa42 100644 --- a/ui/src/components/DeleteModal/DeleteModal.tsx +++ b/ui/src/components/DeleteModal/DeleteModal.tsx @@ -8,7 +8,7 @@ import { _ } from '@splunk/ui-utils/i18n'; import { generateToast } from '../../util/util'; import { StyledButton } from '../../pages/EntryPageStyle'; -import { axiosCallWrapper } from '../../util/axiosCallWrapper'; +import { axiosCallWrapper, generateEndPointUrl } from '../../util/axiosCallWrapper'; import TableContext from '../../context/TableContext'; import { parseErrorMsg, getFormattedMessage } from '../../util/messageUtil'; import { PAGE_INPUT } from '../../constants/pages'; @@ -52,9 +52,11 @@ class DeleteModal extends Component { (prevState) => ({ ...prevState, isDeleting: true, ErrorMsg: '' }), () => { axiosCallWrapper({ - serviceName: `${this.props.serviceName}/${encodeURIComponent( - this.props.stanzaName - )}`, + endpointUrl: generateEndPointUrl( + `${encodeURIComponent(this.props.serviceName)}/${encodeURIComponent( + this.props.stanzaName + )}` + ), customHeaders: { 'Content-Type': 'application/x-www-form-urlencoded' }, method: 'delete', handleError: false, diff --git a/ui/src/components/MultiInputComponent/MultiInputComponent.tsx b/ui/src/components/MultiInputComponent/MultiInputComponent.tsx index 8247af34a..28e2eb446 100644 --- a/ui/src/components/MultiInputComponent/MultiInputComponent.tsx +++ b/ui/src/components/MultiInputComponent/MultiInputComponent.tsx @@ -4,9 +4,10 @@ import styled from 'styled-components'; import WaitSpinner from '@splunk/react-ui/WaitSpinner'; import { z } from 'zod'; -import { AxiosCallType, axiosCallWrapper } from '../../util/axiosCallWrapper'; +import { AxiosCallType, axiosCallWrapper, generateEndPointUrl } from '../../util/axiosCallWrapper'; import { filterResponse } from '../../util/util'; import { MultipleSelectCommonOptions } from '../../types/globalConfig/entities'; +import { invariant } from '../../util/invariant'; const MultiSelectWrapper = styled(Multiselect)` width: 320px !important; @@ -79,19 +80,20 @@ function MultiInputComponent(props: MultiInputComponentProps) { let current = true; const abortController = new AbortController(); + const url = referenceName + ? generateEndPointUrl(encodeURIComponent(referenceName)) + : endpointUrl; + invariant( + url, + '[MultiInputComponent] referenceName or endpointUrl or items must be provided' + ); + const apiCallOptions = { signal: abortController.signal, handleError: true, params: { count: -1 }, - serviceName: '', - endpointUrl: '', + endpointUrl: url, } satisfies AxiosCallType; - if (referenceName) { - apiCallOptions.serviceName = referenceName; - } else if (endpointUrl) { - apiCallOptions.endpointUrl = endpointUrl; - } - if (dependencyValues) { apiCallOptions.params = { ...apiCallOptions.params, ...dependencyValues }; } diff --git a/ui/src/components/SingleInputComponent/SingleInputComponent.tsx b/ui/src/components/SingleInputComponent/SingleInputComponent.tsx index bec9792e8..989ea8cf1 100755 --- a/ui/src/components/SingleInputComponent/SingleInputComponent.tsx +++ b/ui/src/components/SingleInputComponent/SingleInputComponent.tsx @@ -3,16 +3,16 @@ import Select from '@splunk/react-ui/Select'; import Button from '@splunk/react-ui/Button'; import ComboBox from '@splunk/react-ui/ComboBox'; import Clear from '@splunk/react-icons/enterprise/Clear'; -import axios from 'axios'; import styled from 'styled-components'; import WaitSpinner from '@splunk/react-ui/WaitSpinner'; import { z } from 'zod'; -import { axiosCallWrapper } from '../../util/axiosCallWrapper'; +import { AxiosCallType, axiosCallWrapper, generateEndPointUrl } from '../../util/axiosCallWrapper'; import { SelectCommonOptions } from '../../types/globalConfig/entities'; import { filterResponse } from '../../util/util'; import { getValueMapTruthyFalse } from '../../util/considerFalseAndTruthy'; import { StandardPages } from '../../types/components/shareableTypes'; +import { invariant } from '../../util/invariant'; const SelectWrapper = styled(Select)` width: 320px !important; @@ -115,20 +115,22 @@ function SingleInputComponent(props: SingleInputComponentProps) { } let current = true; - const source = axios.CancelToken.source(); + const abortController = new AbortController(); + + const url = referenceName + ? generateEndPointUrl(encodeURIComponent(referenceName)) + : endpointUrl; + invariant( + url, + '[SingleInputComponent] referenceName or endpointUrl or autoCompleteFields must be provided' + ); const backendCallOptions = { - serviceName: '', - endpointUrl: '', - cancelToken: source.token, + signal: abortController.signal, + endpointUrl: url, handleError: true, params: { count: -1 }, - }; - if (referenceName) { - backendCallOptions.serviceName = referenceName; - } else if (endpointUrl) { - backendCallOptions.endpointUrl = endpointUrl; - } + } satisfies AxiosCallType; if (dependencyValues) { backendCallOptions.params = { ...backendCallOptions.params, ...dependencyValues }; @@ -161,9 +163,10 @@ function SingleInputComponent(props: SingleInputComponentProps) { } else { setOptions([]); } + // eslint-disable-next-line consistent-return return () => { - source.cancel('Operation canceled.'); + abortController.abort('Operation canceled.'); current = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/ui/src/components/table/TableWrapper.tsx b/ui/src/components/table/TableWrapper.tsx index 454d3b497..dca8bb3f3 100644 --- a/ui/src/components/table/TableWrapper.tsx +++ b/ui/src/components/table/TableWrapper.tsx @@ -3,7 +3,7 @@ import update from 'immutability-helper'; import axios from 'axios'; import { WaitSpinnerWrapper } from './CustomTableStyle'; -import { axiosCallWrapper } from '../../util/axiosCallWrapper'; +import { axiosCallWrapper, generateEndPointUrl } from '../../util/axiosCallWrapper'; import { getUnifiedConfigs, generateToast } from '../../util/util'; import CustomTable from './CustomTable'; import TableHeader from './TableHeader'; @@ -143,7 +143,7 @@ const TableWrapper: React.FC = ({ const requests = services?.map((service) => axiosCallWrapper({ - serviceName: service.name, + endpointUrl: generateEndPointUrl(encodeURIComponent(service.name)), params: { count: -1 }, signal: abortController.signal, }) @@ -204,7 +204,9 @@ const TableWrapper: React.FC = ({ const body = new URLSearchParams(); body.append('disabled', String(!row.disabled)); axiosCallWrapper({ - serviceName: `${row.serviceName}/${row.name}`, + endpointUrl: generateEndPointUrl( + `${encodeURIComponent(row.serviceName)}/${encodeURIComponent(row.name)}` + ), body, customHeaders: { 'Content-Type': 'application/x-www-form-urlencoded' }, method: 'post', diff --git a/ui/src/public/mockServiceWorker.js b/ui/src/public/mockServiceWorker.js index b65d2b97b..89f49bfe8 100644 --- a/ui/src/public/mockServiceWorker.js +++ b/ui/src/public/mockServiceWorker.js @@ -8,7 +8,7 @@ * - Please do NOT serve this file on production. */ -const PACKAGE_VERSION = '2.4.8' +const PACKAGE_VERSION = '2.4.11' const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() diff --git a/ui/src/types/modules.d.ts b/ui/src/types/modules.d.ts index 21dbdc73b..aa9f8f7bc 100644 --- a/ui/src/types/modules.d.ts +++ b/ui/src/types/modules.d.ts @@ -1,5 +1,38 @@ declare module '@splunk/splunk-utils/config'; -declare module '@splunk/splunk-utils/url'; +declare module '@splunk/splunk-utils/url' { + export type Sharing = '' | 'app' | 'global' | 'system'; + + export interface NamespaceOptions { + app?: string; + owner?: string; + sharing?: Sharing; + } + + export interface ConfigOptions { + /** Config options including `splunkdPath`. Defaults to the value provided by `@splunk/splunk-utils/config`. */ + splunkdPath?: string; + } + + /** + * Creates a fully qualified URL for the specified endpoint. + * For example: + * ``` + * createRESTURL('server/info'); // "/en-US/splunkd/__raw/services/server/info" + * createRESTURL('saved/searches', {app: 'search'}); // "/en-US/splunkd/__raw/servicesNS/-/search/saved/searches" + * ``` + * @param endpoint - An endpoint to a REST API. + * @param namespaceOptions - Optional namespace options. + * @param configOptions - Optional config options. + * @returns The URL of the REST API endpoint. + * @alias createRESTURL + */ + export function createRESTURL( + endpoint: string, + namespaceOptions?: NamespaceOptions, + configOptions?: ConfigOptions + ): string; +} + declare module '@splunk/search-job'; // declaring modules as utils does not seem to have types diff --git a/ui/src/util/axiosCallWrapper.ts b/ui/src/util/axiosCallWrapper.ts index 5c2628af8..8d227df42 100755 --- a/ui/src/util/axiosCallWrapper.ts +++ b/ui/src/util/axiosCallWrapper.ts @@ -1,20 +1,11 @@ import axios, { AxiosRequestConfig } from 'axios'; import { CSRFToken, app } from '@splunk/splunk-utils/config'; import { createRESTURL } from '@splunk/splunk-utils/url'; -import { generateEndPointUrl, generateToast } from './util'; +import { generateToast, getUnifiedConfigs } from './util'; import { parseErrorMsg } from './messageUtil'; -interface axiosCallWithServiceName { - serviceName?: string; +export interface AxiosCallType { endpointUrl: string; -} - -interface axiosCallWithEndpointUrl { - serviceName: string; - endpointUrl?: string; -} - -interface CommonAxiosCall { params?: Record; signal?: AbortSignal; customHeaders?: Record; @@ -24,12 +15,17 @@ interface CommonAxiosCall { callbackOnError?: (error: unknown) => void; } -export type AxiosCallType = (axiosCallWithServiceName | axiosCallWithEndpointUrl) & CommonAxiosCall; +export function generateEndPointUrl(name: string) { + const unifiedConfigs = getUnifiedConfigs(); + + return `${unifiedConfigs.meta.restRoot}_${name}`; +} + +const DEFAULT_PARAMS = { output_mode: 'json' }; /** * * @param {Object} data The object containing required params for request - * @param {string} data.serviceName service name which is input name or tab name based on the page * @param {string} data.endpointUrl rest endpoint path * @param {object} data.params object with params as key value pairs * @param {object} data.body object with body as key value pairs for post request @@ -40,7 +36,6 @@ export type AxiosCallType = (axiosCallWithServiceName | axiosCallWithEndpointUrl * @returns */ const axiosCallWrapper = ({ - serviceName, endpointUrl, params, body, @@ -50,32 +45,25 @@ const axiosCallWrapper = ({ handleError = false, callbackOnError = () => {}, }: AxiosCallType) => { - const endpoint = serviceName ? generateEndPointUrl(serviceName) : endpointUrl; - const appData = { - app, - owner: 'nobody', - }; const baseHeaders = { 'X-Splunk-Form-Key': CSRFToken, 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json', }; const headers = Object.assign(baseHeaders, customHeaders); - const url = createRESTURL(endpoint, appData); + const url = createRESTURL(endpointUrl, { app, owner: 'nobody' }); - let newParams = { output_mode: 'json' }; - if (params) { - newParams = { ...newParams, ...params }; - } - - const options: Record = { - params: newParams, + const options: AxiosRequestConfig = { + params: { + ...DEFAULT_PARAMS, + ...params, + }, method, url, withCredentials: true, headers, signal, - } satisfies AxiosRequestConfig; + }; if (method === 'post') { options.data = body; diff --git a/ui/src/util/util.ts b/ui/src/util/util.ts index 733c77c03..6d9514311 100644 --- a/ui/src/util/util.ts +++ b/ui/src/util/util.ts @@ -24,13 +24,6 @@ export function getMetaInfo() { }; } -export function generateEndPointUrl(name: string) { - if (!unifiedConfigs) { - throw new Error('No GlobalConfig set'); - } - return `${unifiedConfigs.meta.restRoot}_${name}`; -} - export function setUnifiedConfig(unifiedConfig: GlobalConfig) { const result = GlobalConfigSchema.safeParse(unifiedConfig); if (!result.success) {