Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(api): encode URL params #1384

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions ui/src/components/BaseFormView/BaseFormView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -862,7 +862,7 @@ class BaseFormView extends PureComponent<BaseFormProps, BaseFormState> {
}

axiosCallWrapper({
serviceName: this.endpoint,
endpointUrl: generateEndPointUrl(encodeURIComponent(this.endpoint)),
body,
customHeaders: { 'Content-Type': 'application/x-www-form-urlencoded' },
method: 'post',
Expand Down Expand Up @@ -1134,12 +1134,16 @@ class BaseFormView extends PureComponent<BaseFormProps, BaseFormState> {
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<string, AcceptableFormValueOrNullish> = {
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,
Expand All @@ -1159,7 +1163,7 @@ class BaseFormView extends PureComponent<BaseFormProps, BaseFormState> {
}
});

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,
Expand Down
4 changes: 2 additions & 2 deletions ui/src/components/ConfigurationFormView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) => {
Expand Down
10 changes: 6 additions & 4 deletions ui/src/components/DeleteModal/DeleteModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -52,9 +52,11 @@ class DeleteModal extends Component<DeleteModalProps, DeleteModalState> {
(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,
Expand Down
20 changes: 11 additions & 9 deletions ui/src/components/MultiInputComponent/MultiInputComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 };
}
Expand Down
29 changes: 16 additions & 13 deletions ui/src/components/SingleInputComponent/SingleInputComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions ui/src/components/table/TableWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -143,7 +143,7 @@ const TableWrapper: React.FC<ITableWrapperProps> = ({
const requests =
services?.map((service) =>
axiosCallWrapper({
serviceName: service.name,
endpointUrl: generateEndPointUrl(encodeURIComponent(service.name)),
params: { count: -1 },
signal: abortController.signal,
})
Expand Down Expand Up @@ -204,7 +204,9 @@ const TableWrapper: React.FC<ITableWrapperProps> = ({
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',
Expand Down
2 changes: 1 addition & 1 deletion ui/src/public/mockServiceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
35 changes: 34 additions & 1 deletion ui/src/types/modules.d.ts
Original file line number Diff line number Diff line change
@@ -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

Expand Down
44 changes: 16 additions & 28 deletions ui/src/util/axiosCallWrapper.ts
Original file line number Diff line number Diff line change
@@ -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<string, string | number>;
signal?: AbortSignal;
customHeaders?: Record<string, string>;
Expand All @@ -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
Expand All @@ -40,7 +36,6 @@ export type AxiosCallType = (axiosCallWithServiceName | axiosCallWithEndpointUrl
* @returns
*/
const axiosCallWrapper = ({
serviceName,
endpointUrl,
params,
body,
Expand All @@ -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<string, unknown> = {
params: newParams,
const options: AxiosRequestConfig = {
params: {
...DEFAULT_PARAMS,
...params,
},
method,
url,
withCredentials: true,
headers,
signal,
} satisfies AxiosRequestConfig;
};

if (method === 'post') {
options.data = body;
Expand Down
7 changes: 0 additions & 7 deletions ui/src/util/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading