/**
* Copyright 2017 OpenStack Foundation
* Licensed 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 flanguage governing permissions and
* limitations under the License.
**/
import request from 'superagent/lib/client';
import URI from "urijs";
URI.escapeQuerySpace = false;
let http = request;
import Swal from 'sweetalert2';
import T from "i18n-react/dist/i18n-react";
import { isClearingSessionState, setSessionClearingState, getCurrentPathName } from './methods';
import { CLEAR_SESSION_STATE } from '../components/security/actions';
import { doLogin, initLogOut } from '../components/security/methods';
import {CODE_200} from "./constants";
export const GENERIC_ERROR = "Yikes. Something seems to be broken. Our web team has been notified, and we apologize for the inconvenience.";
export const RESET_LOADING = 'RESET_LOADING';
export const START_LOADING = 'START_LOADING';
export const STOP_LOADING = 'STOP_LOADING';
export const VALIDATE = 'VALIDATE';
export const CLEAR_MESSAGE = 'CLEAR_MESSAGE';
export const SHOW_MESSAGE = 'SHOW_MESSAGE';
export const SET_SNACKBAR_MESSAGE = "SET_SNACKBAR_MESSAGE";
export const CLEAR_SNACKBAR_MESSAGE = "CLEAR_SNACKBAR_MESSAGE";
export const createAction = type => payload => ({
type,
payload
});
export const resetLoading = createAction(RESET_LOADING);
export const startLoading = createAction(START_LOADING);
export const stopLoading = createAction(STOP_LOADING);
export const clearSnackbarMessage = () => (dispatch) => {
dispatch(createAction(CLEAR_SNACKBAR_MESSAGE)({}));
};
export const setSnackbarMessage = (message) => (dispatch) => {
dispatch(createAction(SET_SNACKBAR_MESSAGE)(message));
};
// setSnackbarMessage is message-only, so run authErrorHandler's 401 re-login / 403 logout callback here
const notifySnackbarError = (message, callback) => (dispatch) => {
setSnackbarMessage(message)(dispatch);
if (typeof callback === "function") callback();
};
export const snackbarErrorHandler = (err, res) => (dispatch, state) => {
authErrorHandler(err, res, notifySnackbarError)(dispatch, state);
};
export const snackbarSuccessHandler = (message) => (dispatch, state) =>
setSnackbarMessage({ ...message, type: "success", code: CODE_200 })(
dispatch,
state
);
export const snackbarErrorMsg = (message) => (dispatch, state) =>
setSnackbarMessage({ ...message, type: "error", code: CODE_200 })(
dispatch,
state
);
const xhrs = {};
const etagCache = {};
const cancel = (key) => {
if(xhrs[key]) {
xhrs[key].abort();
console.log(`aborted request ${key}`);
delete xhrs[key];
}
}
const schedule = (key, req) => {
// console.log(`scheduling ${key}`);
xhrs[key] = req;
};
const isObjectEmpty = (obj) => {
return Object.keys(obj).length === 0 && obj.constructor === Object ;
}
const buildNotifyHandlerPayload = (httpCode, title, content, type) => ({ httpCode, title, html: content, type });
const buildNotifyHandlerErrorPayload = (httpCode, title, content) => buildNotifyHandlerPayload(httpCode, title, content, "error");
const buildNotifyHandlerWarningPayload = (httpCode, title, content) => buildNotifyHandlerPayload(httpCode, title, content, "warning");
const initLogin = () => (dispatch) => {
const currentLocation = getCurrentPathName();
const clearingSessionState = isClearingSessionState();
dispatch({
type: CLEAR_SESSION_STATE,
payload: {}
});
if (!clearingSessionState) {
setSessionClearingState(true);
console.log("authErrorHandler 401 - re login");
doLogin(currentLocation);
}
};
const normalizeFormDataPayload = (req, formData) => {
if(!isObjectEmpty(formData)) {
Object.keys(formData).forEach(function (key) {
let value = formData[key];
if (Array.isArray(value)) {
value.forEach(item => {
req.field(`${key}[]`, item);
});
} else {
req.field(key, value);
}
});
}
};
export const authErrorHandler = (
err,
res,
notifyErrorHandler = showMessage
) => (dispatch) => {
const code = err.status;
let msg = "";
let payload, callback;
dispatch(stopLoading());
switch (code) {
case 401:
if (notifyErrorHandler !== showMessage) {
payload = buildNotifyHandlerErrorPayload(code, "ERROR", T.translate("errors.user_not_auth"));
callback = () => dispatch(initLogin());
} else {
dispatch(initLogin());
}
break;
case 403:
payload = buildNotifyHandlerErrorPayload(code, "ERROR", T.translate("errors.user_not_authz"));
callback = initLogOut;
break;
case 404:
msg = err.response.body?.message || err.response.error?.message || err.message;
if (err.response.body?.errors?.length) {
msg += `
${err.response.body.errors.join("
")}`;
}
payload = buildNotifyHandlerWarningPayload(code, "Not Found", msg);
break;
case 412:
for (const [key, value] of Object.entries(err.response.body.errors)) {
msg += isNaN(key) ? `${key}: ` : "";
msg += `${value}
`;
}
dispatch({
type: VALIDATE,
payload: { errors: err.response.body.errors }
});
payload = buildNotifyHandlerWarningPayload(code, "Validation error", msg);
break;
default:
payload = buildNotifyHandlerErrorPayload(code, "ERROR", T.translate("errors.server_error"));
}
if (payload)
dispatch(notifyErrorHandler(payload, callback));
}
export const getRequest =(
requestActionCreator,
receiveActionCreator,
endpoint,
errorHandler = defaultErrorHandler,
requestActionPayload = {},
useEtag = false
) => (params = {}) => (dispatch, state) => {
let url = URI(endpoint);
let key = url.toString();
if(!isObjectEmpty(params)) {
// remove the access token
const { access_token: _, ...newParams} = params;
// and generate new key
key = url.query(newParams).toString();
url = url.query(params);
}
if(requestActionCreator && typeof requestActionCreator === 'function')
dispatch(requestActionCreator(requestActionPayload));
cancel(key);
return new Promise((resolve, reject) => {
let req = http.get(url.toString());
if(useEtag && etagCache.hasOwnProperty(key)){
const { etag } = etagCache[key];
if(etag){
req.set('If-None-Match', etag)
}
}
req.timeout({
response: 60000,
deadline: 60000,
})
.end(responseHandler(dispatch, state, receiveActionCreator, errorHandler, resolve, reject, key, useEtag))
schedule(key, req);
});
};
export const putRequest = (
requestActionCreator,
receiveActionCreator,
endpoint,
payload,
errorHandler = defaultErrorHandler,
requestActionPayload = {}
) => (params = {}) => ( dispatch, state) => {
let url = URI(endpoint);
if(!isObjectEmpty(params))
url = url.query(params);
if(requestActionCreator && typeof requestActionCreator === 'function')
dispatch(requestActionCreator(requestActionPayload));
return new Promise((resolve, reject) => {
if(payload == null)
payload = {};
http.put(url.toString())
.send(payload)
.end(responseHandler(dispatch, state, receiveActionCreator, errorHandler, resolve, reject))
});
};
export const deleteRequest = (
requestActionCreator,
receiveActionCreator,
endpoint,
payload,
errorHandler = defaultErrorHandler,
requestActionPayload = {}
) => (params) => (dispatch, state) => {
let url = URI(endpoint);
if(!isObjectEmpty(params))
url = url.query(params);
if(requestActionCreator && typeof requestActionCreator === 'function')
dispatch(requestActionCreator(requestActionPayload));
return new Promise((resolve, reject) => {
if(payload == null)
payload = {};
http.delete(url)
.send(payload)
.end(responseHandler(dispatch, state, receiveActionCreator, errorHandler, resolve, reject));
});
};
export const postRequest = (
requestActionCreator,
receiveActionCreator,
endpoint,
payload,
errorHandler = defaultErrorHandler,
requestActionPayload = {}
) => (params = {}) => (dispatch, state) => {
let url = URI(endpoint);
if(!isObjectEmpty(params))
url = url.query(params);
if(requestActionCreator && typeof requestActionCreator === 'function')
dispatch(requestActionCreator(requestActionPayload));
return new Promise((resolve, reject) => {
let request = http.post(url);
if(payload != null)
request.send(payload);
else // to be a simple CORS request
request.set('Content-Type', 'text/plain');
request.end(responseHandler(dispatch, state, receiveActionCreator, errorHandler, resolve, reject));
});
};
export const postFile = (
requestActionCreator,
receiveActionCreator,
endpoint,
file,
fileMetadata = {},
errorHandler = defaultErrorHandler,
requestActionPayload = {}
) => (params = {}) => (dispatch, state) => {
let url = URI(endpoint);
if(!isObjectEmpty(params))
url = url.query(params);
if(requestActionCreator && typeof requestActionCreator === 'function')
dispatch(requestActionCreator(requestActionPayload));
return new Promise((resolve, reject) => {
const req = http.post(url)
.attach('file', file);
normalizeFormDataPayload(req, fileMetadata);
req.end(responseHandler(dispatch, state, receiveActionCreator, errorHandler, resolve, reject));
});
};
export const putFile = (
requestActionCreator,
receiveActionCreator,
endpoint,
file = null,
fileMetadata = {},
errorHandler = defaultErrorHandler,
requestActionPayload = {}
) => (params = {}) => (dispatch, state) => {
let url = URI(endpoint);
if(!isObjectEmpty(params))
url = url.query(params);
if(requestActionCreator && typeof requestActionCreator === 'function')
dispatch(requestActionCreator(requestActionPayload));
return new Promise((resolve, reject) => {
const req = http.put(url);
if(file != null){
req.attach('file', file);
}
normalizeFormDataPayload(req, fileMetadata)
req.end(responseHandler(dispatch, state, receiveActionCreator, errorHandler, resolve, reject));
});
};
export const defaultErrorHandler = (err, res) => (dispatch) => {
let body = res.body;
let text = '';
if(body instanceof Object){
if(body.hasOwnProperty('message'))
text = body.message;
}
Swal.fire(res.statusText, text, "error");
}
const byLowerCase = toFind => value => toLowerCase(value) === toFind;
const toLowerCase = value => value.toLowerCase();
const getKeys = headers => Object.keys(headers);
export const getHeaderCaseInsensitive = (headerName, headers = {}) => {
const key = getKeys(headers).find(byLowerCase(headerName));
return key ? headers[key] : undefined;
};
export const responseHandler = ( dispatch, state, receiveActionCreator, errorHandler, resolve, reject, key = null, useEtag= false ) =>
(err, res) => {
if (err || !res.ok) {
let code = err.status;
if(code === 304 && etagCache.hasOwnProperty(key) && useEtag){
const { body } = etagCache[key];
if(typeof receiveActionCreator === 'function') {
dispatch(receiveActionCreator({response: body}));
return resolve({response: body});
}
dispatch(receiveActionCreator);
return resolve({response: body});
}
if(errorHandler) {
errorHandler(err, res)(dispatch, state);
}
return reject({ err, res, dispatch, state })
}
let json = res.body;
if(useEtag) {
const responseETAG = getHeaderCaseInsensitive('etag', res.headers);
if (responseETAG) {
etagCache[key] = { etag: responseETAG, body: json};
}
}
if(typeof receiveActionCreator === 'function') {
dispatch(receiveActionCreator({response: json}));
return resolve({response: json});
}
dispatch(receiveActionCreator);
return resolve({response: json});
}
export const fetchErrorHandler = (response) => {
let code = response.status;
let msg = response.statusText;
switch (code) {
case 403:
Swal.fire("ERROR", T.translate("errors.user_not_authz"), "warning");
break;
case 401:
Swal.fire("ERROR", T.translate("errors.session_expired"), "error");
break;
case 412:
Swal.fire("ERROR", msg, "warning");
case 500:
Swal.fire("ERROR", T.translate("errors.server_error"), "error");
}
}
export const fetchResponseHandler = (response) => {
if (!response.ok) {
throw response;
} else {
return response.json();
}
}
export const showMessage = (settings, callback = null) => (dispatch) => {
dispatch(stopLoading());
Swal.fire(settings).then((result) => {
if (result.value && typeof callback === 'function') {
callback();
}
});
}
export const showSuccessMessage = (html) => (dispatch) => {
dispatch(stopLoading());
Swal.fire({
title: T.translate("general.done"),
html: html,
type: 'success'
});
}
export const downloadFileByContent = (filename, content, mime) => {
let link = document.createElement('a');
link.textContent = 'download';
link.download = filename;
link.href = `data:${mime},${encodeURIComponent(content)}`
document.body.appendChild(link); // Required for FF
link.click();
document.body.removeChild(link);
}
export const getCSV = (endpoint, params, filename, header = null) => (dispatch) => {
let url = URI(endpoint);
if(!isObjectEmpty(params))
url = url.query(params);
dispatch(startLoading());
return fetch(url.toString())
.then((response) => {
if (!response.ok) {
throw response;
} else {
return response.text();
}
})
.then((csv) => {
dispatch(stopLoading());
if (header) {
csv = header + '\r\r' + csv;
}
downloadFileByContent(filename, csv, 'text/csv;charset=utf-8');
})
.catch(fetchErrorHandler);
};
export const getRawCSV = (endpoint, params, header = null) => {
let url = URI(endpoint);
if(!isObjectEmpty(params))
url = url.query(params);
return fetch(url.toString())
.then((response) => {
if (!response.ok) {
throw response;
} else {
return response.text();
}
})
.then((csv) => {
if (header) {
csv = header + '\r\r' + csv;
}
return csv;
})
.catch(fetchErrorHandler);
};
export const escapeFilterValue = (value) => {
value = String(value);
// escape backslash first so you don't accidentally break your own escapes
value = value.replace(/\\/g, "\\\\");
value = value.replace(/,/g, "\\,");
value = value.replace(/;/g, "\\;");
// especial case for literal +
value = value.replace(/\+/g, "%2B");
return value;
};