Initial commit: Atomaste website
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
.rsssl-modal-body, .rsssl {
|
||||
input.MuiInput-underline:before {
|
||||
display:none;
|
||||
}
|
||||
.MuiAutocomplete-root {
|
||||
.MuiInputLabel-outlined[data-shrink=false] {
|
||||
transform: translate(14px, 16px) scale(1);
|
||||
}
|
||||
.MuiFormLabel-root {
|
||||
font-family:inherit;
|
||||
}
|
||||
.MuiOutlinedInput-root {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
input.MuiAutocomplete-input[type=text] {
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow:none;
|
||||
}
|
||||
border:0;
|
||||
padding-left:12px;
|
||||
}
|
||||
.MuiInputBase-root {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.MuiInput-root input.MuiInputBase-input {
|
||||
padding-left:10px;
|
||||
}
|
||||
.MuiPopper-root, .MuiPaper-root {
|
||||
max-height:150px;
|
||||
z-index: 999999;
|
||||
div {
|
||||
font-family: inherit !important;
|
||||
}
|
||||
ul {
|
||||
max-height:initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* The native selectControl doesn't allow disabling per option.
|
||||
*/
|
||||
|
||||
import DOMPurify from "dompurify";
|
||||
import {Autocomplete} from "@mui/material";
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import './AutoComplete.scss';
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
autoComplete: {
|
||||
fontSize: "12px"
|
||||
}
|
||||
}));
|
||||
|
||||
const AutoCompleteControl = ({field, disabled, value, options, label, onChange }) => {
|
||||
let selectDisabled = !Array.isArray(disabled) && disabled;
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Autocomplete
|
||||
classes={{
|
||||
input: classes.autoComplete,
|
||||
option: classes.autoComplete
|
||||
}}
|
||||
disabled={selectDisabled}
|
||||
disablePortal
|
||||
value={ value }
|
||||
id={field.id}
|
||||
options={options}
|
||||
isOptionEqualToValue={(option, value) => {
|
||||
const optionValue = typeof option.value === "string" ? option.value.toLowerCase() : option.value;
|
||||
const valueValue = typeof value.value === "string" ? value.value.toLowerCase() : value.value;
|
||||
return optionValue === valueValue;
|
||||
}}
|
||||
getOptionLabel={(option) => {
|
||||
if ( option && option.label ) {
|
||||
return option.label;
|
||||
}
|
||||
const selectedOption = options.find( item => item.value === option );
|
||||
if ( selectedOption ) {
|
||||
return selectedOption.label;
|
||||
}
|
||||
return option;
|
||||
} }
|
||||
onChange={(event, newValue) => {
|
||||
let value = newValue && newValue.value ? newValue.value : '';
|
||||
onChange(value);
|
||||
}}
|
||||
renderInput={(params) => <TextField {...params}
|
||||
label={label}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default AutoCompleteControl
|
||||
@@ -0,0 +1,45 @@
|
||||
import Hyperlink from "../utils/Hyperlink";
|
||||
import * as rsssl_api from "../utils/api";
|
||||
import useFields from "./FieldsData";
|
||||
import Icon from "../utils/Icon";
|
||||
import {useState} from "@wordpress/element";
|
||||
|
||||
/**
|
||||
* Render a help notice in the sidebar
|
||||
*/
|
||||
const Button = (props) => {
|
||||
const {addHelpNotice} = useFields();
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const {fields} = useFields();
|
||||
|
||||
const onClickHandler = async (action) => {
|
||||
let data = {};
|
||||
setProcessing(true);
|
||||
data.fields = fields;
|
||||
await rsssl_api.doAction(action, data).then((response) => {
|
||||
let label = response.success ? 'success' : 'warning';
|
||||
let title = response.title;
|
||||
let text = response.message;
|
||||
setProcessing(false);
|
||||
addHelpNotice(props.field.id, label, text, title, false);
|
||||
});
|
||||
}
|
||||
|
||||
let is_disabled = !!props.field.disabled;
|
||||
|
||||
return (
|
||||
<>
|
||||
{ props.field.url &&
|
||||
<Hyperlink className={"button button-default"} disabled={is_disabled} text={props.field.button_text} url={props.field.url}/>
|
||||
}
|
||||
{ props.field.action &&
|
||||
<button onClick={ () => onClickHandler( props.field.action ) } className="button button-default" disabled={is_disabled}>
|
||||
{props.field.button_text}
|
||||
{processing && <Icon name = "loading" color = 'grey' />}
|
||||
</button>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Button
|
||||
@@ -0,0 +1,89 @@
|
||||
import ReCaptcha from './ReCaptcha';
|
||||
import HCaptcha from './HCaptcha';
|
||||
import useFields from '../FieldsData';
|
||||
import useCaptchaData from "./CaptchaData";
|
||||
import {__} from '@wordpress/i18n';
|
||||
import {useEffect, useState} from "@wordpress/element";
|
||||
import ErrorBoundary from "../../utils/ErrorBoundary";
|
||||
import Button from "../Button";
|
||||
|
||||
const Captcha = ({props}) => {
|
||||
const {getFieldValue, updateField, saveFields, getField} = useFields();
|
||||
const enabled_captcha_provider = getFieldValue('enabled_captcha_provider');
|
||||
const siteKey = getFieldValue(`${enabled_captcha_provider}_site_key`);
|
||||
const secretKey = getFieldValue(`${enabled_captcha_provider}_secret_key` );
|
||||
const fully_enabled = getFieldValue('captcha_fully_enabled');
|
||||
const {verifyCaptcha, setReloadCaptcha, removeRecaptchaScript} = useCaptchaData();
|
||||
const [showCaptcha, setShowCaptcha] = useState(false);
|
||||
const [buttonEnabled, setButtonEnabled] = useState(false);
|
||||
|
||||
|
||||
|
||||
const handleCaptchaResponse = (response) => {
|
||||
verifyCaptcha(response).then((response) => {
|
||||
if (response && response.success) {
|
||||
updateField('captcha_fully_enabled', 1);
|
||||
saveFields(false, false, true);
|
||||
} else {
|
||||
updateField('captcha_fully_enabled', false);
|
||||
saveFields(false, false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//if we switch to another captcha provider, we need to reset the captcha
|
||||
useEffect(() => {
|
||||
saveFields(false, false);
|
||||
}, [enabled_captcha_provider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fully_enabled) {
|
||||
updateField('captcha_fully_enabled', 1);
|
||||
saveFields(false, false);
|
||||
}
|
||||
}, [fully_enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowCaptcha(false);
|
||||
//based on the provider the keys need certain length if hcapthca the length is 36 and recapthca 40
|
||||
switch (enabled_captcha_provider) {
|
||||
case 'recaptcha':
|
||||
if (siteKey.length === 40 && secretKey.length === 40) {
|
||||
setButtonEnabled(true);
|
||||
} else {
|
||||
setButtonEnabled(false);
|
||||
}
|
||||
break;
|
||||
case 'hcaptcha':
|
||||
if (siteKey.length === 36 && secretKey.length === 35) {
|
||||
setButtonEnabled(true);
|
||||
} else {
|
||||
setButtonEnabled(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}, [siteKey, secretKey, enabled_captcha_provider]);
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ErrorBoundary title={__('Reload Captcha' , 'really-simple-ssl')}>
|
||||
{enabled_captcha_provider === 'recaptcha' && !fully_enabled && showCaptcha && (
|
||||
<ReCaptcha handleCaptchaResponse={handleCaptchaResponse} />
|
||||
)}
|
||||
{enabled_captcha_provider === 'hcaptcha' && !fully_enabled && showCaptcha && (
|
||||
<HCaptcha sitekey={siteKey} handleCaptchaResponse={handleCaptchaResponse} captchaVerified={fully_enabled}/>
|
||||
)}
|
||||
{enabled_captcha_provider !== 'none' && !fully_enabled && (
|
||||
<button
|
||||
disabled={!buttonEnabled}
|
||||
className={`button button-primary ${!buttonEnabled ? 'rsssl-learning-mode-disabled' : ''}`}
|
||||
// style={{display: !showCaptcha? 'none': 'block'}}
|
||||
onClick={() => setShowCaptcha(true)}> {__('Validate CAPTCHA', 'really-simple-ssl')} </button>)
|
||||
}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Captcha;
|
||||
@@ -0,0 +1,40 @@
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
|
||||
const useCaptchaData = create(( set, get ) => ({
|
||||
reloadCaptcha: false,
|
||||
setReloadCaptcha: ( value ) => set({ reloadCaptcha: value }),
|
||||
verifyCaptcha: async ( responseToken ) => {
|
||||
try {
|
||||
const response = await rsssl_api.doAction('verify_captcha', { responseToken: responseToken });
|
||||
|
||||
// Handle the response
|
||||
if ( !response ) {
|
||||
console.error('No response received from the server.');
|
||||
return;
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
},
|
||||
removeRecaptchaScript: async(source = 'recaptcha') => {
|
||||
if (window.grecaptcha) {
|
||||
window.grecaptcha.reset();
|
||||
delete window.grecaptcha;
|
||||
}
|
||||
const scriptTags = document.querySelectorAll('script[src^="https://www.google.com/recaptcha/api.js"]');
|
||||
// For each found script tag
|
||||
scriptTags.forEach((scriptTag) => {
|
||||
scriptTag.remove(); // Remove it
|
||||
});
|
||||
const rescriptTags = document.querySelectorAll('script[src^="https://www.google.com/recaptcha/api.js"]');
|
||||
// now we check if reCaptcha was still rendered.
|
||||
const recaptchaContainer = document.getElementById('recaptchaContainer');
|
||||
if (recaptchaContainer) {
|
||||
recaptchaContainer.remove();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export default useCaptchaData;
|
||||
@@ -0,0 +1,39 @@
|
||||
import Icon from "../../utils/Icon";
|
||||
import useFields from "../FieldsData";
|
||||
import {TextControl} from "@wordpress/components"; // assuming you're using WordPress components
|
||||
|
||||
const CaptchaKey = ({ field, fields, label }) => {
|
||||
const { getFieldValue, setChangedField, updateField, saveFields} = useFields();
|
||||
|
||||
let fieldValue = getFieldValue(field.id);
|
||||
let captchaVerified = getFieldValue('captcha_fully_enabled');
|
||||
|
||||
const onChangeHandler = async (fieldValue) => {
|
||||
setChangedField(field.id, fieldValue);
|
||||
setChangedField('captcha_fully_enabled', false);
|
||||
updateField(field.id, fieldValue);
|
||||
await saveFields(false, false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextControl
|
||||
required={field.required}
|
||||
placeholder={field.placeholder}
|
||||
help={field.comment}
|
||||
label={label}
|
||||
onChange={(value) => onChangeHandler(value)}
|
||||
value={fieldValue}
|
||||
/>
|
||||
|
||||
<div className="rsssl-email-verified" >
|
||||
{Boolean(captchaVerified)
|
||||
? <Icon name='circle-check' color={'green'} />
|
||||
: <Icon name='circle-times' color={'red'} />
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CaptchaKey;
|
||||
@@ -0,0 +1,46 @@
|
||||
import {useEffect} from "@wordpress/element";
|
||||
const HCaptcha = ({ sitekey, handleCaptchaResponse }) => {
|
||||
const hcaptchaCallback = (response) => {
|
||||
handleCaptchaResponse(response);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const script = document.createElement('script');
|
||||
|
||||
script.src = `https://hcaptcha.com/1/api.js?onload=initHcaptcha`;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
|
||||
script.onload = () => {
|
||||
if (typeof window.hcaptcha !== 'undefined') {
|
||||
window.hcaptcha.render('hcaptchaContainer', {
|
||||
sitekey: sitekey,
|
||||
callback: hcaptchaCallback
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.body.appendChild(script);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
// Check if hcaptcha is loaded before trying to remove it
|
||||
if (window.hcaptcha) {
|
||||
window.hcaptcha.reset();
|
||||
}
|
||||
if (script) {
|
||||
script.remove();
|
||||
}
|
||||
};
|
||||
|
||||
}, [sitekey, handleCaptchaResponse]);
|
||||
|
||||
return (
|
||||
<div className="rsssl-captcha"
|
||||
style={{display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '20px'}}>
|
||||
<div id='hcaptchaContainer'></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HCaptcha;
|
||||
@@ -0,0 +1,62 @@
|
||||
import {useEffect} from "@wordpress/element";
|
||||
import useFields from '../FieldsData';
|
||||
import CaptchaData from "./CaptchaData";
|
||||
|
||||
/**
|
||||
* ReCaptcha functionality.
|
||||
*
|
||||
* @param {function} handleCaptchaResponse - The callback function to handle the ReCaptcha response.
|
||||
* @param {boolean} captchaVerified - Boolean value indicating whether the ReCaptcha is verified or not.
|
||||
* @return {JSX.Element} - The ReCaptcha component JSX.
|
||||
*/
|
||||
const ReCaptcha = ({ handleCaptchaResponse , captchaVerified}) => {
|
||||
const recaptchaCallback = (response) => {
|
||||
handleCaptchaResponse(response);
|
||||
};
|
||||
|
||||
const {reloadCaptcha, removeRecaptchaScript, setReloadCaptcha} = CaptchaData();
|
||||
const {getFieldValue, updateField, saveFields} = useFields();
|
||||
const sitekey = getFieldValue('recaptcha_site_key');
|
||||
const secret = getFieldValue('recaptcha_secret_key');
|
||||
const fully_enabled = getFieldValue('captcha_fully_enabled');
|
||||
|
||||
useEffect(() => {
|
||||
const script = document.createElement('script');
|
||||
|
||||
script.src = `https://www.google.com/recaptcha/api.js?render=explicit&onload=initRecaptcha`;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
|
||||
script.onload = () => {
|
||||
// We restore the recaptcha script if it was not removed.
|
||||
let recaptchaContainer = document.getElementById('recaptchaContainer');
|
||||
if (typeof window.grecaptcha !== 'undefined') {
|
||||
window.initRecaptcha = window.initRecaptcha || (() => {
|
||||
window.grecaptcha && window.grecaptcha.render(recaptchaContainer, {
|
||||
sitekey: sitekey,
|
||||
callback: recaptchaCallback,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.body.appendChild(script);
|
||||
|
||||
}, [sitekey, handleCaptchaResponse]);
|
||||
|
||||
useEffect(() => {
|
||||
// Move cleanup here.
|
||||
if (captchaVerified) {
|
||||
removeRecaptchaScript();
|
||||
}
|
||||
}, [captchaVerified]);
|
||||
|
||||
return (
|
||||
<div className="rsssl-captcha"
|
||||
style={{display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '20px'}} >
|
||||
<div id='recaptchaContainer'></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReCaptcha;
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* The tooltip can't be included in the native toggleControl, so we have to build our own.
|
||||
*/
|
||||
import { useState, useRef, useEffect } from "@wordpress/element";
|
||||
import { __experimentalConfirmDialog as ConfirmDialog } from '@wordpress/components';
|
||||
import hoverTooltip from "../utils/hoverTooltip";
|
||||
import {__} from '@wordpress/i18n';
|
||||
|
||||
const CheckboxControl = (props) => {
|
||||
|
||||
const checkboxRef = useRef(null);
|
||||
|
||||
let disabledCheckboxPropBoolean = (props.disabled === true);
|
||||
let disabledCheckboxViaFieldConfig = (props.field.disabled === true);
|
||||
|
||||
let checkboxDisabled = (
|
||||
disabledCheckboxViaFieldConfig
|
||||
|| disabledCheckboxPropBoolean
|
||||
);
|
||||
|
||||
let tooltipText = '';
|
||||
let emptyValues = [undefined, null, ''];
|
||||
|
||||
if (checkboxDisabled
|
||||
&& props.field.hasOwnProperty('disabledTooltipHoverText')
|
||||
&& !emptyValues.includes(props.field.disabledTooltipHoverText)
|
||||
) {
|
||||
tooltipText = props.field.disabledTooltipHoverText;
|
||||
}
|
||||
|
||||
hoverTooltip(
|
||||
checkboxRef,
|
||||
(checkboxDisabled && (tooltipText !== '')),
|
||||
tooltipText
|
||||
);
|
||||
|
||||
// const tooltipText = __("404 errors detected on your home page. 404 blocking is unavailable, to prevent blocking of legitimate visitors. It is strongly recommended to resolve these errors.", "really-simple-ssl");
|
||||
// // Pass props.disabled as the condition
|
||||
// hoverTooltip(checkboxRef, props.disabled, tooltipText);
|
||||
|
||||
const [ isOpen, setIsOpen ] = useState( false );
|
||||
const onChangeHandler = (e) => {
|
||||
// WordPress <6.0 does not have the confirmdialog component
|
||||
if ( !ConfirmDialog ) {
|
||||
executeAction();
|
||||
return;
|
||||
}
|
||||
if (props.field.warning && props.field.warning.length>0 && !props.field.value) {
|
||||
setIsOpen( true );
|
||||
} else {
|
||||
executeAction();
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
setIsOpen( false );
|
||||
executeAction();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsOpen( false );
|
||||
};
|
||||
|
||||
const executeAction = (e) => {
|
||||
let fieldValue = !props.field.value;
|
||||
props.onChangeHandler(fieldValue)
|
||||
}
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
onChangeHandler(true);
|
||||
}
|
||||
}
|
||||
let field = props.field;
|
||||
let is_checked = field.value ? 'is-checked' : '';
|
||||
let is_disabled = props.disabled ? 'is-disabled' : '';
|
||||
|
||||
return (
|
||||
<>
|
||||
{ConfirmDialog && <ConfirmDialog
|
||||
isOpen={ isOpen }
|
||||
onConfirm={ handleConfirm }
|
||||
onCancel={ handleCancel }
|
||||
>
|
||||
{field.warning}
|
||||
</ConfirmDialog> }
|
||||
<div className="components-base-control components-toggle-control">
|
||||
<div className="components-base-control__field">
|
||||
<div data-wp-component="HStack" className="components-flex components-h-stack">
|
||||
<span className={ "components-form-toggle "+is_checked + ' ' +is_disabled}>
|
||||
<input
|
||||
ref={checkboxRef}
|
||||
onKeyDown={(e) => handleKeyDown(e)}
|
||||
checked={props.value}
|
||||
className="components-form-toggle__input"
|
||||
onChange={ ( e ) => onChangeHandler(e) }
|
||||
id={props.id}
|
||||
type="checkbox"
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
<span className="components-form-toggle__track"></span>
|
||||
<span className="components-form-toggle__thumb"></span>
|
||||
</span>
|
||||
<label htmlFor={field.id} className="components-toggle-control__label">{props.label}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default CheckboxControl
|
||||
@@ -0,0 +1,26 @@
|
||||
.rsssl-datatable-component {
|
||||
|
||||
.rsssl-action-buttons__inner {
|
||||
.rsssl-action-buttons__button {
|
||||
&.rsssl-red {
|
||||
border: 0 solid transparent;
|
||||
background: var(--rsp-red);
|
||||
color: var(--rsp-text-color-white);
|
||||
|
||||
&:hover {
|
||||
background: var(--rsp-dark-red);
|
||||
color: var(--rsp-text-color-white);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rsssl-add-button__button, .rsssl-action-buttons__button {
|
||||
//display: flex;
|
||||
|
||||
.rsssl-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import DataTableStore from "../DataTableStore";
|
||||
import './Buttons.scss'
|
||||
import Icon from "../../../utils/Icon";
|
||||
const ControlButton = ({ controlButton }) => {
|
||||
const {
|
||||
processing,
|
||||
} = DataTableStore();
|
||||
return (
|
||||
<div className="rsssl-add-button">
|
||||
<div className="rsssl-add-button__inner">
|
||||
<button
|
||||
className="button button-secondary button-datatable rsssl-add-button__button"
|
||||
onClick={controlButton.onClick}
|
||||
disabled={processing}
|
||||
>
|
||||
{processing && <Icon name = "loading" color = 'grey' />}
|
||||
{controlButton.label}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ControlButton;
|
||||
@@ -0,0 +1,24 @@
|
||||
import DataTableStore from "../DataTableStore";
|
||||
import './Buttons.scss'
|
||||
import Icon from "../../../utils/Icon";
|
||||
import {memo} from "@wordpress/element";
|
||||
|
||||
const MultiSelectButton = ({ids, buttonData}) => {
|
||||
const {
|
||||
processing,
|
||||
rowAction,
|
||||
} = DataTableStore();
|
||||
return (
|
||||
<div className={`rsssl-action-buttons__inner`}>
|
||||
<button
|
||||
className={`button ${buttonData.className} rsssl-action-buttons__button`}
|
||||
onClick={(e) => rowAction(ids, buttonData.action, buttonData.type, buttonData.reloadFields) }
|
||||
disabled={processing}
|
||||
>
|
||||
{processing && <Icon name = "loading" color = 'grey' />}
|
||||
{buttonData.label}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default memo(MultiSelectButton)
|
||||
@@ -0,0 +1,23 @@
|
||||
import DataTableStore from "../DataTableStore";
|
||||
import './Buttons.scss'
|
||||
import Icon from "../../../utils/Icon";
|
||||
import {memo} from "@wordpress/element";
|
||||
|
||||
const RowButton = ({id, buttonData}) => {
|
||||
const {
|
||||
processing,
|
||||
rowAction,
|
||||
} = DataTableStore();
|
||||
return (
|
||||
<div className={`rsssl-action-buttons__inner`}>
|
||||
<button
|
||||
className={`button ${buttonData.className} rsssl-action-buttons__button`}
|
||||
onClick={(e) => rowAction([id], buttonData.action, buttonData.type, buttonData.reloadFields) }
|
||||
disabled={processing}
|
||||
>
|
||||
{buttonData.label}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default memo(RowButton);
|
||||
@@ -0,0 +1,16 @@
|
||||
//style for checkbox when some rows are selected
|
||||
.rsssl-indeterminate {
|
||||
input[name="select-all-rows"] {
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="10" y="45" width="80" height="10" fill="currentColor"/></svg>') no-repeat center center;
|
||||
}
|
||||
}
|
||||
|
||||
//style for checkbox when all rows are selected
|
||||
.rsssl-all-selected {
|
||||
input[name="select-all-rows"]::before {
|
||||
content: url(data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%3Cpath%20d%3D%27M14.83%204.89l1.34.94-5.81%208.38H9.02L5.78%209.67l1.34-1.25%202.57%202.4z%27%20fill%3D%27%233582c4%27%2F%3E%3C%2Fsvg%3E);
|
||||
margin: -0.1875rem 0 0 -0.25rem;
|
||||
height: 1.3125rem;
|
||||
width: 1.3125rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
.rsssl-datatable-component {
|
||||
margin-left: calc(0px - var(--rsp-spacing-l));
|
||||
margin-right: calc(0px - var(--rsp-spacing-l));
|
||||
>div {
|
||||
//prevent scrollbar on datatable
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.rdt_TableCol, .rdt_TableCell, .rdt_TableCol_Sortable {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.rdt_TableCol:first-child, .rdt_TableCell:first-child {
|
||||
min-width: initial;
|
||||
}
|
||||
|
||||
.rdt_TableHeadRow {
|
||||
.rdt_TableCol:last-child {
|
||||
flex-grow: 0;
|
||||
flex-direction: row-reverse;
|
||||
min-width: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.rdt_TableRow {
|
||||
&:nth-child(odd) {
|
||||
background-color: var(--rsp-grey-200)
|
||||
}
|
||||
|
||||
padding: var(--rsp-spacing-xs) 0;
|
||||
.rdt_TableCell:last-child {
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//wp-core also adds an svg for the select dropdown, so we hide the one from the react datatables component
|
||||
nav.rdt_Pagination > div > svg {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.rsssl-container {
|
||||
padding: 2em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import {produce} from "immer";
|
||||
|
||||
const DataTableStore = create((set, get) => ({
|
||||
|
||||
processing: false,
|
||||
dataLoaded: false,
|
||||
dataActions: {},
|
||||
sourceData: [],
|
||||
filteredData: [],
|
||||
searchTerm:'',
|
||||
searchColumns:[],
|
||||
reloadFields:false,
|
||||
setReloadFields: (reloadFields) => set({reloadFields}),
|
||||
clearAllData: () => set({sourceData: [], filteredData: []}),
|
||||
setProcessing: (processing) => set({processing}),
|
||||
fetchData: async (action, dataActions) => {
|
||||
set({processing: true});
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action,
|
||||
dataActions
|
||||
);
|
||||
if (response && response.data ) {
|
||||
set({filteredData:response.data, sourceData: response.data, dataLoaded: true, processing: false});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
handleSearch: (searchTerm, searchColumns) => {
|
||||
set({searchTerm})
|
||||
set({searchColumns})
|
||||
let data = get().sourceData;
|
||||
const filteredData = data.filter(item =>
|
||||
searchColumns.some(column =>
|
||||
item[column] && item[column].toLowerCase().includes(searchTerm.toLowerCase())
|
||||
));
|
||||
set({filteredData: filteredData});
|
||||
},
|
||||
/*
|
||||
* This function handles the filter, it is called from the GroupSetting class
|
||||
*/
|
||||
handleFilter: async (column, filterValue) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, filterColumn: column, filterValue};
|
||||
})
|
||||
);
|
||||
},
|
||||
restoreView: () => {
|
||||
//filter the data again
|
||||
let searchTerm = get().searchTerm;
|
||||
if ( searchTerm !== '' ) {
|
||||
let searchColumns = get().searchColumns;
|
||||
get().handleSearch(searchTerm, searchColumns);
|
||||
}
|
||||
},
|
||||
//only removes rows from the dataset clientside, does not do an API call
|
||||
removeRows:(ids) => {
|
||||
let filteredData = get().filteredData;
|
||||
let sourceData = get().sourceData;
|
||||
let newFilteredData = filteredData.filter(item => !ids.includes(item.id));
|
||||
let newSourceData = sourceData.filter(item => !ids.includes(item.id));
|
||||
set({filteredData: newFilteredData, sourceData: newSourceData});
|
||||
get().restoreView();
|
||||
},
|
||||
rowAction: async ( ids, action, actionType, reloadFields ) => {
|
||||
actionType = typeof actionType !== 'undefined' ? actionType : '';
|
||||
set({processing: true});
|
||||
if ( actionType === 'delete' ) {
|
||||
get().removeRows(ids);
|
||||
}
|
||||
let data = {
|
||||
ids: ids,
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action,
|
||||
data
|
||||
);
|
||||
|
||||
if ( response.data ) {
|
||||
set({filteredData:response.data, sourceData: response.data, dataLoaded: true, processing: false});
|
||||
get().restoreView();
|
||||
if (reloadFields) {
|
||||
get().setReloadFields(reloadFields);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export default DataTableStore;
|
||||
@@ -0,0 +1,176 @@
|
||||
import {useEffect, useState , memo } from "@wordpress/element";
|
||||
import DataTable, { createTheme } from "react-data-table-component";
|
||||
|
||||
import DataTableStore from "../DataTable/DataTableStore";
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import ControlButton from "../DataTable/Buttons/ControlButton";
|
||||
import RowButton from "../DataTable/Buttons/RowButton";
|
||||
import SearchBar from "../DataTable/SearchBar/SearchBar";
|
||||
import SelectedRowsControl from "../DataTable/SelectedRowsControl/SelectedRowsControl";
|
||||
import './DataTable.scss';
|
||||
import './Checkboxes.scss';
|
||||
import useFields from "../FieldsData";
|
||||
import useMenu from "../../Menu/MenuData";
|
||||
|
||||
const DataTableWrapper = ({field, controlButton, enabled}) => {
|
||||
const {
|
||||
filteredData,
|
||||
handleSearch,
|
||||
dataLoaded,
|
||||
fetchData,
|
||||
reloadFields,
|
||||
} = DataTableStore();
|
||||
const {fetchFieldsData} = useFields();
|
||||
const {selectedSubMenuItem} = useMenu();
|
||||
const [rowsSelected, setRowsSelected] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
|
||||
useEffect(() => {
|
||||
if ( !dataLoaded) {
|
||||
fetchData(field.action, {});
|
||||
}
|
||||
}, [dataLoaded] );
|
||||
|
||||
useEffect(() => {
|
||||
if ( reloadFields ) {
|
||||
fetchFieldsData(selectedSubMenuItem);
|
||||
}
|
||||
}, [reloadFields]);
|
||||
|
||||
/**
|
||||
* Build a column configuration object.
|
||||
*
|
||||
* @param {object} column - The column object.
|
||||
* @param {string} column.name - The name of the column.
|
||||
* @param {boolean} column.sortable - Whether the column is sortable.
|
||||
* @param {boolean} column.searchable - Whether the column is searchable.
|
||||
* @param {number} column.width - The width of the column.
|
||||
* @param {boolean} column.visible - Whether the column is visible.
|
||||
* @param {string} column.column - The column identifier.
|
||||
*
|
||||
* @returns {object} The column configuration object.
|
||||
*/
|
||||
const buildColumn = ({reloadFields, name, isButton, action, label, className, sortable, searchable, width, visible, column}) => ({
|
||||
reloadFields, name, isButton, action, label, className, sortable, searchable, width, visible, column, selector: row => row[column],
|
||||
});
|
||||
const columns = field.columns.map(buildColumn);
|
||||
const buttonColumns = columns.filter(column => column.isButton);
|
||||
const hasSelectableRows = buttonColumns.length>0;
|
||||
const searchableColumns = columns.filter(column => column.searchable).map(column => column.column);
|
||||
|
||||
const customgitStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light');
|
||||
|
||||
const handleSelection = ({selectedCount, selectedRows}) => {
|
||||
// based on the current page and the rows per page we get the rows that are selected
|
||||
let actualRows = rowsPerPage;
|
||||
//in case not all selected, get the rows that are selected from the current page.
|
||||
//the datatable component selects 'all' rows, but we only want the rows from the current page.
|
||||
let rows = [];
|
||||
if ( selectedCount < rowsPerPage ) {
|
||||
rows = selectedRows;
|
||||
setRowsSelected(selectedRows);
|
||||
} else if ( selectedCount >= rowsPerPage ) {
|
||||
//previously all rows were selected, but now some were unselected.
|
||||
//in the latter case we need to get the rows that are selected from the current page.
|
||||
//remove the rows from all pages after the current page
|
||||
let diff = filteredData.length - selectedRows.length;
|
||||
rows = selectedRows.slice( 0, (currentPage * rowsPerPage) - diff );
|
||||
if ( currentPage > 1 ) {
|
||||
//remove the rows from all pages before the current page from the selected rows
|
||||
rows = rows.slice( (currentPage - 1) * rowsPerPage);
|
||||
}
|
||||
setRowsSelected(rows);
|
||||
}
|
||||
}
|
||||
|
||||
const data= dataLoaded && filteredData.length>0 ? {...filteredData} : [];
|
||||
for (const key in data) {
|
||||
const dataItem = {...data[key]};
|
||||
//check if there exists a column with column = 'actionButton'
|
||||
if ( buttonColumns.length > 0 ) {
|
||||
for (const buttonColumn of buttonColumns) {
|
||||
dataItem[buttonColumn.column] = <RowButton id={dataItem.id} buttonData={buttonColumn}/>
|
||||
}
|
||||
}
|
||||
data[key] = dataItem;
|
||||
}
|
||||
let selectAllRowsClass = "";
|
||||
if ( rowsSelected.length>0 && rowsSelected.length < rowsPerPage) {
|
||||
selectAllRowsClass = "rsssl-indeterminate";
|
||||
}
|
||||
if ( rowsSelected.length === rowsPerPage ) {
|
||||
selectAllRowsClass = "rsssl-all-selected";
|
||||
}
|
||||
return (
|
||||
<div className={"rsssl-datatable-component"}>
|
||||
<div className="rsssl-container">
|
||||
{controlButton.show && <ControlButton controlButton={controlButton}/> }
|
||||
{/*Ensure that positioning also works without the addButton, by adding a div */}
|
||||
{ !controlButton.show && <div></div>}
|
||||
<SearchBar
|
||||
handleSearch={handleSearch}
|
||||
searchableColumns={searchableColumns}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ field.multiselect_buttons && rowsSelected.length > 0 && (
|
||||
<SelectedRowsControl rowsSelected={rowsSelected} buttonData = {field.multiselect_buttons} />
|
||||
)}
|
||||
|
||||
<DataTable
|
||||
className={ selectAllRowsClass }
|
||||
columns={columns}
|
||||
data={Object.values(data)}
|
||||
dense
|
||||
pagination={true}
|
||||
paginationComponentOptions={{
|
||||
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
|
||||
rangeSeparatorText: __('of', 'really-simple-ssl'),
|
||||
noRowsPerPage: false,
|
||||
selectAllRowsItem: false,
|
||||
selectAllRowsItemText: __('All', 'really-simple-ssl'),
|
||||
}}
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
selectableRows={hasSelectableRows}
|
||||
//clearSelectedRows={() => setRowsSelected([])}
|
||||
paginationPerPage={rowsPerPage}
|
||||
onChangePage={setCurrentPage}
|
||||
onChangeRowsPerPage={setRowsPerPage}
|
||||
onSelectedRowsChange={handleSelection}
|
||||
theme="really-simple-plugins"
|
||||
// customStyles={customStyles}
|
||||
/>
|
||||
{!enabled && (
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay">
|
||||
<span className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span>
|
||||
<span>{__('Here you can add IP addresses that should never be blocked by region restrictions.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(DataTableWrapper);
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import './SearchBar.scss';
|
||||
import {memo} from "@wordpress/element";
|
||||
|
||||
const SearchBar = ({ handleSearch, searchableColumns }) => {
|
||||
const [debounceTimer, setDebounceTimer] = useState(null);
|
||||
|
||||
const onKeyUp = (event) => {
|
||||
clearTimeout(debounceTimer);
|
||||
setDebounceTimer(setTimeout(() => {
|
||||
handleSearch(event.target.value, searchableColumns)
|
||||
}, 500));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rsssl-search-bar">
|
||||
<div className="rsssl-search-bar__inner">
|
||||
<div className="rsssl-search-bar__icon"></div>
|
||||
<input
|
||||
type="text"
|
||||
className="rsssl-search-bar__input"
|
||||
placeholder={__("Search", "really-simple-ssl")}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(SearchBar);
|
||||
@@ -0,0 +1,32 @@
|
||||
.rsssl-search-bar {
|
||||
float: right;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.rsssl-search-bar__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.rsssl-search-bar__inner:focus-within {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.rsssl-search-bar__icon {
|
||||
/* Add styles for the search icon */
|
||||
}
|
||||
|
||||
.rsssl-search-bar__input {
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 3px 5px;
|
||||
width: 150px; /* Adjust width as needed */
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.rsssl-search-bar__input:focus {
|
||||
width: 200px; /* Adjust width as needed */
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import {__, _n} from "@wordpress/i18n";
|
||||
import DataTableStore from "../DataTableStore";
|
||||
import MultiSelectButton from "../Buttons/MultiSelectButton";
|
||||
import './SelectedRowsControl.scss'
|
||||
import {memo} from "@wordpress/element";
|
||||
import MenuItem from "../../../Menu/MenuItem";
|
||||
|
||||
const SelectedRowsControl = ({ rowsSelected, buttonData }) => {
|
||||
const {
|
||||
processing,
|
||||
filteredData,
|
||||
} = DataTableStore();
|
||||
//ensure that all items in the rowsSelected array still exist in the filteredData array
|
||||
//after a delete this might not be the case
|
||||
let rowsSelectedFiltered = rowsSelected.filter(selectedRow =>
|
||||
filteredData.some(filteredRow => filteredRow.id === selectedRow.id)
|
||||
);
|
||||
|
||||
if ( rowsSelectedFiltered.length === 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//parse ids from rowsSelected into array
|
||||
const ids = rowsSelectedFiltered.map((row) => row.id);
|
||||
return (
|
||||
<div className="rsssl-selected-rows-control">
|
||||
<div className={"rsssl-multiselect-datatable-form rsssl-primary"}>
|
||||
<div>
|
||||
{_n( "You have selected %d row", "You have selected %d rows", rowsSelectedFiltered.length, 'really-simple-ssl' ).replace('%d', rowsSelectedFiltered.length )}
|
||||
</div>
|
||||
<div className="rsssl-action-buttons">
|
||||
<>
|
||||
{ buttonData.map((buttonItem, i) => <MultiSelectButton key={"multiselectButton-"+i} ids={ids} buttonData={buttonItem} /> ) }
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(SelectedRowsControl);
|
||||
@@ -0,0 +1,14 @@
|
||||
.rsssl-selected-rows-control {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
|
||||
//blue container above datatable for multiselect
|
||||
.rsssl-multiselect-datatable-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
Justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 1em 2em;
|
||||
background: var(--rsp-blue-faded);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
const AddButton = ({ getCurrentFilter, moduleName, handleOpen, processing, blockedText, allowedText }) => {
|
||||
let buttonText = getCurrentFilter(moduleName) === 'blocked' ? blockedText : allowedText;
|
||||
|
||||
return (
|
||||
<div className="rsssl-add-button">
|
||||
{(getCurrentFilter(moduleName) === 'blocked' || getCurrentFilter(moduleName) === 'allowed') && (
|
||||
<div className="rsssl-add-button__inner">
|
||||
<button
|
||||
className="button button-secondary button-datatable rsssl-add-button__button"
|
||||
onClick={handleOpen}
|
||||
disabled={processing}
|
||||
>
|
||||
{buttonText}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddButton;
|
||||
@@ -0,0 +1,178 @@
|
||||
import {__} from '@wordpress/i18n';
|
||||
import {useRef, useEffect, useState} from '@wordpress/element';
|
||||
import DataTable, {createTheme} from "react-data-table-component";
|
||||
import useFields from "../FieldsData";
|
||||
import DynamicDataTableStore from "./DynamicDataTableStore";
|
||||
|
||||
const DynamicDataTable = (props) => {
|
||||
const {
|
||||
twoFAMethods,
|
||||
setTwoFAMethods,
|
||||
DynamicDataTable,
|
||||
dataLoaded,
|
||||
pagination,
|
||||
dataActions,
|
||||
handleTableRowsChange,
|
||||
fetchDynamicData,
|
||||
// setDynamicData,
|
||||
handleTableSort,
|
||||
handleTablePageChange,
|
||||
handleTableSearch,
|
||||
} = DynamicDataTableStore();
|
||||
|
||||
let field = props.field;
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const {fields, getFieldValue, saveFields} = useFields();
|
||||
|
||||
const twoFAEnabledRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
twoFAEnabledRef.current = getFieldValue('login_protection_enabled');
|
||||
saveFields(true, false)
|
||||
}, [getFieldValue('login_protection_enabled')]);
|
||||
|
||||
useEffect(() => {
|
||||
const value = getFieldValue('login_protection_enabled');
|
||||
setEnabled(value);
|
||||
}, [fields]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dataLoaded || enabled !== getFieldValue('login_protection_enabled')) {
|
||||
fetchDynamicData(field.action)
|
||||
.then(response => {
|
||||
// Check if response.data is defined and is an array before calling reduce
|
||||
if(response.data && Array.isArray(response.data)) {
|
||||
const methods = response.data.reduce((acc, user) => ({...acc, [user.id]: user.rsssl_two_fa_status}), {});
|
||||
setTwoFAMethods(methods);
|
||||
} else {
|
||||
console.error('Unexpected response:', response);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err); // Log any errors
|
||||
});
|
||||
}
|
||||
}, [dataLoaded, field.action, fetchDynamicData, getFieldValue('login_protection_enabled')]); // Add getFieldValue('login_protection_enabled') as a dependency
|
||||
|
||||
useEffect(() => {
|
||||
if (dataActions) {
|
||||
fetchDynamicData(field.action);
|
||||
}
|
||||
}, [dataActions]);
|
||||
|
||||
function buildColumn(column) {
|
||||
let newColumn = {
|
||||
name: column.name,
|
||||
column: column.column,
|
||||
sortable: column.sortable,
|
||||
searchable: column.searchable,
|
||||
width: column.width,
|
||||
visible: column.visible,
|
||||
selector: row => row[column.column],
|
||||
};
|
||||
|
||||
return newColumn;
|
||||
}
|
||||
|
||||
let columns = [];
|
||||
|
||||
field.columns.forEach(function (item, i) {
|
||||
let newItem = { ...item, key: item.column };
|
||||
newItem = buildColumn(newItem);
|
||||
newItem.visible = newItem.visible ?? true;
|
||||
columns.push(newItem);
|
||||
});
|
||||
|
||||
let searchableColumns = columns
|
||||
.filter(column => column.searchable)
|
||||
.map(column => column.column);
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-search-bar">
|
||||
<div className="rsssl-search-bar__inner">
|
||||
<div className="rsssl-search-bar__icon"></div>
|
||||
<input
|
||||
type="text"
|
||||
className="rsssl-search-bar__input"
|
||||
placeholder={__("Search", "really-simple-ssl")}
|
||||
onChange={event => handleTableSearch(event.target.value, searchableColumns)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{dataLoaded ?
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={DynamicDataTable}
|
||||
dense
|
||||
pagination
|
||||
paginationServer
|
||||
onChangeRowsPerPage={handleTableRowsChange}
|
||||
onChangePage={handleTablePageChange}
|
||||
sortServer
|
||||
onSort={handleTableSort}
|
||||
paginationRowsPerPageOptions={[10, 25, 50, 100]}
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
theme="really-simple-plugins"
|
||||
customStyles={customStyles}
|
||||
></DataTable>
|
||||
:
|
||||
<div className="rsssl-spinner" style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: "100px"
|
||||
}}>
|
||||
<div className="rsssl-spinner__inner">
|
||||
<div className="rsssl-spinner__icon" style={{
|
||||
border: '8px solid white',
|
||||
borderTop: '8px solid #f4bf3e',
|
||||
borderRadius: '50%',
|
||||
width: '120px',
|
||||
height: '120px',
|
||||
animation: 'spin 2s linear infinite'
|
||||
}}></div>
|
||||
<div className="rsssl-spinner__text" style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}>{__("Loading data, please stand by...", "really-simple-ssl")}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{ !enabled &&
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay"><span
|
||||
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Activate Two-Factor Authentication to enable this block.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
|
||||
}
|
||||
export default DynamicDataTable;
|
||||
@@ -0,0 +1,77 @@
|
||||
/* Creates A Store For Risk Data using Zustand */
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import {produce} from "immer";
|
||||
import React, {useState} from "react";
|
||||
|
||||
const DynamicDataTableStore = create((set, get) => ({
|
||||
|
||||
twoFAMethods: {},
|
||||
setTwoFAMethods: (methods) => set((state) => ({ ...state, twoFAMethods: methods })),
|
||||
processing: false,
|
||||
dataLoaded: false,
|
||||
pagination: {},
|
||||
dataActions: {},
|
||||
DynamicDataTable: [],
|
||||
fetchDynamicData: async (action) => {
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action,
|
||||
get().dataActions
|
||||
);
|
||||
let data = Array.isArray(response.data) ? response.data : [];
|
||||
let pagination = response.pagination ? response.pagination : 1;
|
||||
//now we set the EventLog
|
||||
if ( response ) {
|
||||
set(state => ({
|
||||
...state,
|
||||
DynamicDataTable: data,
|
||||
dataLoaded: true,
|
||||
processing: false,
|
||||
pagination: pagination,
|
||||
// Removed the twoFAMethods set from here...
|
||||
}));
|
||||
// Return the response for the calling function to use
|
||||
return response;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
|
||||
handleTableSearch: async (search, searchColumns) => {
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, search, searchColumns};
|
||||
}));
|
||||
},
|
||||
|
||||
|
||||
handleTablePageChange: async (page, pageSize) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, page, pageSize};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
handleTableRowsChange: async (currentRowsPerPage, currentPage) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, currentRowsPerPage, currentPage};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
//this handles all pagination and sorting
|
||||
handleTableSort: async (column, sortDirection) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, sortColumn: column, sortDirection};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
export default DynamicDataTableStore;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
const SearchBar = ({ handleSearch, searchableColumns }) => {
|
||||
const [debounceTimer, setDebounceTimer] = useState(null);
|
||||
|
||||
const onKeyUp = (event) => {
|
||||
clearTimeout(debounceTimer);
|
||||
setDebounceTimer(setTimeout(() => {
|
||||
handleSearch(event.target.value, searchableColumns)
|
||||
}, 500));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rsssl-search-bar">
|
||||
<div className="rsssl-search-bar__inner">
|
||||
<div className="rsssl-search-bar__icon"></div>
|
||||
<input
|
||||
type="text"
|
||||
className="rsssl-search-bar__input"
|
||||
placeholder={__("Search", "really-simple-ssl")}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchBar;
|
||||
@@ -0,0 +1,253 @@
|
||||
import {__} from '@wordpress/i18n';
|
||||
import {useEffect, useState} from '@wordpress/element';
|
||||
import DataTable, {createTheme} from "react-data-table-component";
|
||||
import EventLogDataTableStore from "./EventLogDataTableStore";
|
||||
import FilterData from "../FilterData";
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import useMenu from "../../Menu/MenuData";
|
||||
import Flag from "../../utils/Flag/Flag";
|
||||
import Icon from "../../utils/Icon";
|
||||
import useFields from "../FieldsData";
|
||||
import SearchBar from "../DynamicDataTable/SearchBar";
|
||||
|
||||
const EventLogDataTable = (props) => {
|
||||
const {
|
||||
DynamicDataTable,
|
||||
dataLoaded,
|
||||
pagination,
|
||||
dataActions,
|
||||
handleEventTableRowsChange,
|
||||
fetchDynamicData,
|
||||
handleEventTableSort,
|
||||
handleEventTablePageChange,
|
||||
handleEventTableSearch,
|
||||
handleEventTableFilter,
|
||||
processing,
|
||||
rowCleared,
|
||||
} = EventLogDataTableStore()
|
||||
//here we set the selectedFilter from the Settings group
|
||||
const {
|
||||
selectedFilter,
|
||||
setSelectedFilter,
|
||||
activeGroupId,
|
||||
getCurrentFilter,
|
||||
setProcessingFilter,
|
||||
} = FilterData();
|
||||
|
||||
|
||||
|
||||
const {fields, fieldAlreadyEnabled, getFieldValue} = useFields();
|
||||
const [tableHeight, setTableHeight] = useState(600); // Starting height
|
||||
const rowHeight = 50; // Height of each row.
|
||||
const moduleName = 'rsssl-group-filter-' + props.field.id;
|
||||
let field = props.field;
|
||||
useEffect(() => {
|
||||
const currentFilter = getCurrentFilter(moduleName);
|
||||
|
||||
if (!currentFilter) {
|
||||
setSelectedFilter('all', moduleName);
|
||||
}
|
||||
setProcessingFilter(processing);
|
||||
handleEventTableFilter('severity', currentFilter);
|
||||
}, [moduleName, handleEventTableFilter, getCurrentFilter(moduleName), setSelectedFilter, moduleName, DynamicDataTable, processing]);
|
||||
|
||||
|
||||
//if the dataActions are changed, we fetch the data
|
||||
useEffect(() => {
|
||||
//we make sure the dataActions are changed in the store before we fetch the data
|
||||
if (dataActions) {
|
||||
fetchDynamicData(field.action, field.event_type, dataActions)
|
||||
}
|
||||
}, [dataActions.sortDirection, dataActions.filterValue, dataActions.search, dataActions.page, dataActions.currentRowsPerPage]);
|
||||
|
||||
|
||||
|
||||
//we create the columns
|
||||
let columns = [];
|
||||
//getting the fields from the props
|
||||
|
||||
//we loop through the fields
|
||||
field.columns.forEach(function (item, i) {
|
||||
let newItem = buildColumn(item)
|
||||
columns.push(newItem);
|
||||
});
|
||||
|
||||
let enabled = fieldAlreadyEnabled('event_log_enabled');
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for head cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for data cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light');
|
||||
|
||||
//only show the datatable if the data is loaded
|
||||
if (!dataLoaded && columns.length === 0 && DynamicDataTable.length === 0) {
|
||||
return (
|
||||
<div className="rsssl-spinner">
|
||||
<div className="rsssl-spinner__inner">
|
||||
<div className="rsssl-spinner__icon"></div>
|
||||
<div className="rsssl-spinner__text">{__("Loading...", "really-simple-ssl")}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let searchableColumns = [];
|
||||
//setting the searchable columns
|
||||
columns.map(column => {
|
||||
if (column.searchable) {
|
||||
searchableColumns.push(column.column);
|
||||
}
|
||||
});
|
||||
let data = [];
|
||||
|
||||
if (DynamicDataTable.data) {
|
||||
data = DynamicDataTable.data.map((dataItem) => {
|
||||
let newItem = {...dataItem};
|
||||
newItem.iso2_code = generateFlag(newItem.iso2_code, newItem.country_name);
|
||||
newItem.expandableRows = true;
|
||||
return newItem;
|
||||
});
|
||||
}
|
||||
|
||||
//we generate an expandable row
|
||||
const ExpandableRow = ({data}) => {
|
||||
let code, icon, color = '';
|
||||
switch (data.severity) {
|
||||
case 'warning':
|
||||
code = 'rsssl-warning';
|
||||
icon = 'circle-times';
|
||||
color = 'red';
|
||||
break;
|
||||
case 'informational':
|
||||
code = 'rsssl-primary';
|
||||
icon = 'info';
|
||||
color = 'black';
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
code = 'rsssl-primary';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"rsssl-wizard-help-notice " + code}
|
||||
style={{padding: '1em', borderRadius: '5px'}}>
|
||||
{/*now we place a block to the rightcorner with the severity*/}
|
||||
<div style={{float: 'right'}}>
|
||||
<Icon name={icon} color={color}/>
|
||||
</div>
|
||||
<div style={{fontSize: '1em', fontWeight: 'bold'}}>
|
||||
{data.severity.charAt(0).toUpperCase() + data.severity.slice(1)}
|
||||
</div>
|
||||
<div>{data.description}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
function generateFlag(flag, title) {
|
||||
return (
|
||||
<>
|
||||
<Flag
|
||||
countryCode={flag}
|
||||
style={{
|
||||
fontSize: '2em',
|
||||
marginLeft: '0.3em',
|
||||
}}
|
||||
title={title}
|
||||
></Flag>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
let paginationSet;
|
||||
paginationSet = typeof pagination !== 'undefined';
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(data).length === 0 ) {
|
||||
setTableHeight(100); // Adjust depending on your UI measurements
|
||||
} else {
|
||||
setTableHeight(rowHeight * (paginationSet ? pagination.perPage + 2 : 12)); // Adjust depending on your UI measurements
|
||||
}
|
||||
|
||||
}, [paginationSet, pagination?.perPage, data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-container">
|
||||
<div></div>
|
||||
{/*Display the search bar*/}
|
||||
<SearchBar handleSearch={handleEventTableSearch} searchableColumns={searchableColumns}/>
|
||||
</div>
|
||||
{/*Display the datatable*/}
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={processing? [] : data}
|
||||
dense
|
||||
pagination={!processing}
|
||||
paginationServer
|
||||
paginationTotalRows={paginationSet? pagination.totalRows: 10}
|
||||
paginationPerPage={paginationSet? pagination.perPage: 10}
|
||||
paginationDefaultPage={paginationSet?pagination.currentPage: 1}
|
||||
paginationComponentOptions={{
|
||||
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
|
||||
rangeSeparatorText: __('of', 'really-simple-ssl'),
|
||||
noRowsPerPage: false,
|
||||
selectAllRowsItem: false,
|
||||
selectAllRowsItemText: __('All', 'really-simple-ssl'),
|
||||
|
||||
}}
|
||||
onChangeRowsPerPage={handleEventTableRowsChange}
|
||||
onChangePage={handleEventTablePageChange}
|
||||
expandableRows={!processing}
|
||||
expandableRowsComponent={ExpandableRow}
|
||||
loading={dataLoaded}
|
||||
onSort={handleEventTableSort}
|
||||
sortServer={!processing}
|
||||
paginationRowsPerPageOptions={[5, 10, 25, 50, 100]}
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
theme="really-simple-plugins"
|
||||
customStyles={customStyles}
|
||||
></DataTable>
|
||||
{!enabled && (
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay"><span
|
||||
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Activate Limit login attempts to enable this block.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
export default EventLogDataTable;
|
||||
|
||||
function buildColumn(column) {
|
||||
return {
|
||||
name: column.name,
|
||||
sortable: column.sortable,
|
||||
searchable: column.searchable,
|
||||
width: column.width,
|
||||
visible: column.visible,
|
||||
column: column.column,
|
||||
selector: row => row[column.column],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/* Creates A Store For Risk Data using Zustand */
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import {produce} from "immer";
|
||||
import React from "react";
|
||||
|
||||
const EventLogDataTableStore = create((set, get) => ({
|
||||
|
||||
processing: false,
|
||||
dataLoaded: false,
|
||||
pagination: {},
|
||||
dataActions: {},
|
||||
DynamicDataTable: [],
|
||||
sorting: [],
|
||||
rowCleared: false,
|
||||
|
||||
fetchDynamicData: async (action, event_type, dataActions = {}) => {
|
||||
//cool we can fetch the data so first we set the processing to true
|
||||
set({processing: true});
|
||||
set({dataLoaded: false});
|
||||
set({rowCleared: true});
|
||||
if (Object.keys(dataActions).length === 0) {
|
||||
dataActions = get().dataActions;
|
||||
}
|
||||
// add the data_type to the dataActions
|
||||
dataActions = {...dataActions, event_type};
|
||||
//now we fetch the data
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action,
|
||||
dataActions
|
||||
);
|
||||
// now we set the EventLog
|
||||
if (response) {
|
||||
set({DynamicDataTable: response, dataLoaded: true, processing: false, pagination: response.pagination, sorting: response.sorting});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
set({processing: false});
|
||||
set({rowCleared: false});
|
||||
}
|
||||
},
|
||||
|
||||
handleEventTableSearch: async (search, searchColumns) => {
|
||||
//Add the search to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, search, searchColumns};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
handleEventTablePageChange: async (page, pageSize) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, page, pageSize};
|
||||
})
|
||||
);
|
||||
},
|
||||
handleEventTableRowsChange: async (currentRowsPerPage, currentPage) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, currentRowsPerPage, currentPage};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
//this handles all pagination and sorting
|
||||
handleEventTableSort: async (column, sortDirection) => {
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, sortColumn: column, sortDirection};
|
||||
})
|
||||
);
|
||||
},
|
||||
handleEventTableFilter: async (column, filterValue) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, filterColumn: column, filterValue};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
}));
|
||||
|
||||
export default EventLogDataTableStore;
|
||||
@@ -0,0 +1,625 @@
|
||||
import {
|
||||
TextControl,
|
||||
RadioControl,
|
||||
TextareaControl,
|
||||
__experimentalNumberControl as NumberControl
|
||||
} from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import License from "./License/License";
|
||||
import Password from "./Password";
|
||||
import SelectControl from "./SelectControl";
|
||||
import Host from "./Host/Host";
|
||||
import Hyperlink from "../utils/Hyperlink";
|
||||
import LetsEncrypt from "../LetsEncrypt/LetsEncrypt";
|
||||
import Activate from "../LetsEncrypt/Activate";
|
||||
import MixedContentScan from "./MixedContentScan/MixedContentScan";
|
||||
import PermissionsPolicy from "./PermissionsPolicy";
|
||||
import CheckboxControl from "./CheckboxControl";
|
||||
import Support from "./Support";
|
||||
import LearningMode from "./LearningMode/LearningMode";
|
||||
import RiskComponent from "./RiskConfiguration/RiskComponent";
|
||||
import VulnerabilitiesOverview from "./RiskConfiguration/vulnerabilitiesOverview";
|
||||
import IpAddressDatatable from "./LimitLoginAttempts/IpAddressDatatable";
|
||||
import TwoFaRolesDropDown from "./TwoFA/TwoFaRolesDropDown";
|
||||
import Button from "./Button";
|
||||
import Icon from "../utils/Icon";
|
||||
import { useEffect, useState, useRef } from "@wordpress/element";
|
||||
import useFields from "./FieldsData";
|
||||
import PostDropdown from "./PostDropDown";
|
||||
import NotificationTester from "./RiskConfiguration/NotificationTester";
|
||||
import getAnchor from "../utils/getAnchor";
|
||||
import useMenu from "../Menu/MenuData";
|
||||
import UserDatatable from "./LimitLoginAttempts/UserDatatable";
|
||||
import CountryDatatable from "./LimitLoginAttempts/CountryDatatable";
|
||||
import BlockListDatatable from "./GeoBlockList/BlockListDatatable";
|
||||
import TwoFaDataTable from "./TwoFA/TwoFaDataTable";
|
||||
import EventLogDataTable from "./EventLog/EventLogDataTable";
|
||||
import DOMPurify from "dompurify";
|
||||
import RolesDropDown from "./RolesDropDown";
|
||||
import GeoDatatable from "./GeoBlockList/GeoDatatable";
|
||||
import WhiteListDatatable from "./GeoBlockList/WhiteListDatatable";
|
||||
import Captcha from "./Captcha/Captcha";
|
||||
import CaptchaKey from "./Captcha/CaptchaKey";
|
||||
import FileChangeDetection from "./FileChangeDetection/FileChangeDetection";
|
||||
import UserAgentTable from "./firewall/UserAgentTable";
|
||||
import TwoFaEnabledDropDown from "./TwoFA/TwoFaEnabledDropDown";
|
||||
const Field = (props) => {
|
||||
const scrollAnchor = useRef(null);
|
||||
const { updateField, setChangedField, highLightField, setHighLightField , getFieldValue} = useFields();
|
||||
const [anchor, setAnchor] = useState(null);
|
||||
const { selectedFilter, setSelectedFilter } = useMenu();
|
||||
|
||||
useEffect(() => {
|
||||
// Check URL for anchor and highlightfield parameters
|
||||
const anchor = getAnchor('anchor');
|
||||
const highlightField = getAnchor('highlightfield');
|
||||
|
||||
setAnchor(anchor);
|
||||
|
||||
if (highlightField) {
|
||||
setHighLightField(highlightField);
|
||||
}
|
||||
|
||||
if (highlightField === props.field.id && scrollAnchor.current) {
|
||||
scrollAnchor.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
//if the field is a captcha provider, scroll to the captcha provider is a temp fix cause i can't get the scroll to work properly.
|
||||
if (highLightField === 'enabled_captcha_provider' && props.fields) {
|
||||
let captchaField = document.getElementsByClassName('rsssl-highlight')[0];
|
||||
if (captchaField) {
|
||||
captchaField.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
|
||||
if (anchor && anchor === props.field.id) {
|
||||
scrollAnchor.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
handleAnchor();
|
||||
}, [anchor]);
|
||||
|
||||
window.addEventListener('hashchange', () => {
|
||||
const anchor = getAnchor('anchor');
|
||||
const highlightField = getAnchor('highlightfield');
|
||||
|
||||
setAnchor(anchor);
|
||||
|
||||
if (highlightField) {
|
||||
setHighLightField(highlightField);
|
||||
}
|
||||
|
||||
if (highlightField === props.field.id && scrollAnchor.current) {
|
||||
scrollAnchor.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
if (anchor && anchor === props.field.id) {
|
||||
scrollAnchor.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
});
|
||||
|
||||
const handleAnchor = () => {
|
||||
if (anchor && anchor === props.field.id) {
|
||||
scrollAnchor.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeHandler = (fieldValue) => {
|
||||
let field = props.field;
|
||||
if (field.pattern) {
|
||||
const regex = new RegExp(field.pattern, 'g');
|
||||
const allowedCharactersArray = fieldValue.match(regex);
|
||||
fieldValue = allowedCharactersArray ? allowedCharactersArray.join('') : '';
|
||||
}
|
||||
updateField(field.id, fieldValue);
|
||||
|
||||
// We can configure other fields if a field is enabled, or set to a certain value.
|
||||
let configureFieldCondition = false;
|
||||
if (field.configure_on_activation) {
|
||||
if (field.configure_on_activation.hasOwnProperty('condition') && props.field.value == field.configure_on_activation.condition) {
|
||||
configureFieldCondition = true;
|
||||
}
|
||||
let configureField = field.configure_on_activation[0];
|
||||
for (let fieldId in configureField) {
|
||||
if (configureFieldCondition && configureField.hasOwnProperty(fieldId)) {
|
||||
updateField(fieldId, configureField[fieldId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setChangedField(field.id, fieldValue);
|
||||
};
|
||||
|
||||
const labelWrap = (field) => {
|
||||
let tooltipColor = field.warning ? 'red' : 'black';
|
||||
return (
|
||||
<>
|
||||
<div className="cmplz-label-text">{field.label}</div>
|
||||
{field.tooltip && <Icon name="info-open" tooltip={field.tooltip} color={tooltipColor} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
let field = props.field;
|
||||
let fieldValue = field.value;
|
||||
let disabled = field.disabled;
|
||||
let highLightClass = 'rsssl-field-wrap';
|
||||
if (highLightField === props.field.id) {
|
||||
highLightClass = 'rsssl-field-wrap rsssl-highlight';
|
||||
}
|
||||
|
||||
let options = [];
|
||||
if (field.options) {
|
||||
for (let key in field.options) {
|
||||
if (field.options.hasOwnProperty(key)) {
|
||||
let item = {};
|
||||
item.label = field.options[key];
|
||||
item.value = key;
|
||||
options.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a feature can only be used on networkwide or single site setups, pass that info here.
|
||||
if (!rsssl_settings.networkwide_active && field.networkwide_required) {
|
||||
disabled = true;
|
||||
field.comment = (
|
||||
<>
|
||||
{__("This feature is only available networkwide.", "really-simple-ssl")}
|
||||
<Hyperlink target="_blank" rel="noopener noreferrer" text={__("Network settings", "really-simple-ssl")} url={rsssl_settings.network_link} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.conditionallyDisabled) {
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
if (!field.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( field.type==='checkbox' ) {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<CheckboxControl
|
||||
label={labelWrap(field)}
|
||||
field={field}
|
||||
disabled={disabled}
|
||||
onChangeHandler={ ( fieldValue ) => onChangeHandler( fieldValue ) }
|
||||
/>
|
||||
{ field.comment &&
|
||||
<div className="rsssl-comment" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(field.comment) }} />
|
||||
/* nosemgrep: react-dangerouslysetinnerhtml */
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ( field.type==='hidden' ){
|
||||
return (
|
||||
<input type="hidden" value={field.value}/>
|
||||
);
|
||||
}
|
||||
|
||||
if ( field.type==='radio' ){
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<RadioControl
|
||||
label={labelWrap(field)}
|
||||
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
|
||||
selected={ fieldValue }
|
||||
options={ options }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type==='email'){
|
||||
const sendVerificationEmailField = props.fields.find(field => field.id === 'send_verification_email');
|
||||
const emailIsVerified = sendVerificationEmailField && (sendVerificationEmailField.disabled === false);
|
||||
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor} style={{position: 'relative'}}>
|
||||
<TextControl
|
||||
required={ field.required }
|
||||
placeholder={ field.placeholder }
|
||||
disabled={ disabled }
|
||||
help={ field.comment }
|
||||
label={labelWrap(field)}
|
||||
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
|
||||
value= { fieldValue }
|
||||
/>
|
||||
{ sendVerificationEmailField &&
|
||||
<div className="rsssl-email-verified" >
|
||||
{!emailIsVerified
|
||||
? <Icon name='circle-check' color={'green'} />
|
||||
: <Icon name='circle-times' color={'red'} />}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type==='captcha_key') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor} style={{position: 'relative'}}>
|
||||
<CaptchaKey field={field} fields={props.fields} label={labelWrap(field)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if ( field.type==='number' ){
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor} style={{ position: 'relative'}}>
|
||||
<NumberControl
|
||||
required={ field.required }
|
||||
placeholder={ field.placeholder }
|
||||
className="number_full"
|
||||
disabled={ disabled }
|
||||
help={ field.comment }
|
||||
label={labelWrap(field)}
|
||||
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
|
||||
value= { fieldValue }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (field.type==='text' ){
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor} style={{position: 'relative'}}>
|
||||
<TextControl
|
||||
required={ field.required }
|
||||
placeholder={ field.placeholder }
|
||||
disabled={ disabled }
|
||||
help={ field.comment }
|
||||
label={labelWrap(field)}
|
||||
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
|
||||
value= { fieldValue }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if ( field.type==='button' ){
|
||||
return (
|
||||
<div className={'rsssl-field-button ' + highLightClass} ref={scrollAnchor}>
|
||||
<label>{field.label}</label>
|
||||
<Button field={field}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ( field.type==='password' ){
|
||||
return (
|
||||
<div className={ highLightClass} ref={scrollAnchor}>
|
||||
<Password
|
||||
index={ props.index }
|
||||
field={ field }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ( field.type==='textarea' ) {
|
||||
// Handle csp_frame_ancestors_urls differently. Disable on select change
|
||||
let fieldDisabled = false
|
||||
if ( field.id === 'csp_frame_ancestors_urls') {
|
||||
if ( getFieldValue('csp_frame_ancestors') === 'disabled' ) {
|
||||
fieldDisabled = true
|
||||
}
|
||||
} else {
|
||||
fieldDisabled = field.disabled
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<TextareaControl
|
||||
label={ field.label }
|
||||
help={ field.comment }
|
||||
value= { fieldValue }
|
||||
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
|
||||
disabled={ fieldDisabled }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ( field.type==='license' ){
|
||||
let field = props.field;
|
||||
let fieldValue = field.value;
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<License index={props.index} field={field} fieldValue={fieldValue}/>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
if ( field.type==='number' ){
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<NumberControl
|
||||
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
|
||||
help={ field.comment }
|
||||
label={ field.label }
|
||||
value= { fieldValue }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if ( field.type==='email' ){
|
||||
return (
|
||||
<div className={this.highLightClass} ref={this.scrollAnchor}>
|
||||
<TextControl
|
||||
help={ field.comment }
|
||||
label={ field.label }
|
||||
onChange={ ( fieldValue ) => this.onChangeHandler(fieldValue) }
|
||||
value= { fieldValue }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ( field.type==='host') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<Host
|
||||
index={props.index}
|
||||
field={props.field}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if ( field.type==='select') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<SelectControl
|
||||
disabled={ disabled }
|
||||
label={labelWrap(field)}
|
||||
onChangeHandler={ ( fieldValue ) => onChangeHandler(fieldValue) }
|
||||
value= { fieldValue }
|
||||
options={ options }
|
||||
field={field}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if ( field.type==='support' ) {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<Support/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if ( field.type==='postdropdown' ) {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<PostDropdown field={props.field}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if ( field.type==='permissionspolicy' ) {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<PermissionsPolicy disabled={disabled} field={props.field} options={options}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type==='captcha') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<Captcha field={field} label={labelWrap(field)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if ( field.type==='learningmode' ) {
|
||||
return(
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<LearningMode disabled={disabled} field={props.field}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if ( field.type==='riskcomponent' ) {
|
||||
return (<div className={highLightClass} ref={scrollAnchor}>
|
||||
<RiskComponent field={props.field}/>
|
||||
</div>)
|
||||
}
|
||||
|
||||
if ( field.type === 'mixedcontentscan' ) {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<MixedContentScan field={props.field}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'vulnerabilitiestable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<VulnerabilitiesOverview field={props.field} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'two_fa_roles') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<label htmlFor={`rsssl-two-fa-dropdown-${field.id}`}>
|
||||
{labelWrap(field)}
|
||||
</label>
|
||||
<TwoFaRolesDropDown field={props.field} forcedRoledId={props.field.forced_roles_id} optionalRolesId={props.field.optional_roles_id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === 'eventlog-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<EventLogDataTable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (field.type === 'twofa-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<TwoFaDataTable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'ip-address-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<IpAddressDatatable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'user-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<UserDatatable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'file-change-detection') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<FileChangeDetection
|
||||
field={props.field}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'country-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<CountryDatatable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'geo-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<GeoDatatable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'geo-ip-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<WhiteListDatatable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'blocklist-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<BlockListDatatable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'user-agents-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<UserAgentTable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'roles_dropdown') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<label htmlFor={`rsssl-two-fa-dropdown-${field.id}`}>
|
||||
{labelWrap(field)}
|
||||
</label>
|
||||
<RolesDropDown field={props.field}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === 'roles_enabled_dropdown') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<label htmlFor={`rsssl-two-fa-dropdown-${field.id}`}>
|
||||
{labelWrap(field)}
|
||||
</label>
|
||||
<TwoFaEnabledDropDown field={props.field} disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if(field.type === 'notificationtester') {
|
||||
return (
|
||||
<div className={'rsssl-field-button ' + highLightClass} ref={scrollAnchor}>
|
||||
<NotificationTester field={props.field} disabled={disabled} labelWrap={labelWrap}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if ( field.type === 'letsencrypt' ) {
|
||||
return (
|
||||
<LetsEncrypt field={field} />
|
||||
)
|
||||
}
|
||||
|
||||
if ( field.type === 'activate' ) {
|
||||
return (
|
||||
<Activate field={field}/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
'not found field type '+field.type
|
||||
);
|
||||
}
|
||||
|
||||
export default Field;
|
||||
@@ -0,0 +1,311 @@
|
||||
import {create} from 'zustand';
|
||||
import {produce} from 'immer';
|
||||
import * as rsssl_api from "../utils/api";
|
||||
import {__} from '@wordpress/i18n';
|
||||
import {toast} from 'react-toastify';
|
||||
import {ReactConditions} from "../utils/ReactConditions";
|
||||
|
||||
const fetchFields = () => {
|
||||
return rsssl_api.getFields().then((response) => {
|
||||
let fields = response.fields;
|
||||
let progress = response.progress;
|
||||
let error = response.error;
|
||||
return {fields, progress, error};
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
const useFields = create(( set, get ) => ({
|
||||
fieldsLoaded: false,
|
||||
error:false,
|
||||
fields: [],
|
||||
changedFields:[],
|
||||
progress:[],
|
||||
nextButtonDisabled:false,
|
||||
overrideNextButtonDisabled:false,
|
||||
refreshTests:false,
|
||||
highLightField: '',
|
||||
setHighLightField: (highLightField) => {
|
||||
set({ highLightField });
|
||||
},
|
||||
|
||||
setRefreshTests: (refreshTests) => set(state => ({ refreshTests })),
|
||||
handleNextButtonDisabled: (nextButtonDisabled) => {
|
||||
set({overrideNextButtonDisabled: nextButtonDisabled});
|
||||
},
|
||||
setChangedField: (id, value) => {
|
||||
set(
|
||||
produce((state) => {
|
||||
//remove current reference
|
||||
const existingFieldIndex = state.changedFields.findIndex(field => {
|
||||
return field.id===id;
|
||||
});
|
||||
|
||||
if (existingFieldIndex!==-1){
|
||||
state.changedFields.splice(existingFieldIndex, 1);
|
||||
}
|
||||
|
||||
//add again, with new value
|
||||
let field = {};
|
||||
field.id = id;
|
||||
field.value = value;
|
||||
state.changedFields.push(field);
|
||||
})
|
||||
)
|
||||
},
|
||||
showSavedSettingsNotice : (text , type = 'success') => {
|
||||
handleShowSavedSettingsNotice(text, type);
|
||||
},
|
||||
|
||||
updateField: (id, value) => {
|
||||
set(
|
||||
produce((state) => {
|
||||
let index = state.fields.findIndex(fieldItem => fieldItem.id === id);
|
||||
if (index !== -1) {
|
||||
state.fields[index].value = value;
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
updateFieldAttribute: (id, attribute, value) => {
|
||||
set(
|
||||
produce((state) => {
|
||||
let index = state.fields.findIndex(fieldItem => fieldItem.id === id);
|
||||
if (index !== -1) {
|
||||
state.fields[index][attribute] = value;
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
updateSubField: (id, subItemId, value) => {
|
||||
set(
|
||||
produce((state) => {
|
||||
let index = state.fields.findIndex(fieldItem => fieldItem.id === id);
|
||||
let itemValue = state.fields[index].value;
|
||||
if (!Array.isArray(itemValue)) {
|
||||
itemValue = [];
|
||||
}
|
||||
|
||||
let subIndex = itemValue.findIndex(subItem => subItem.id === subItemId);
|
||||
if (subIndex !== -1) {
|
||||
state.fields[index].updateItemId = subItemId;
|
||||
state.fields[index].value[subIndex]['value'] = value;
|
||||
state.fields[index].value = itemValue.map(item => {
|
||||
const { deleteControl, valueControl, statusControl, ...rest } = item;
|
||||
return rest;
|
||||
});
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
removeHelpNotice: (id) => {
|
||||
set(
|
||||
produce((state) => {
|
||||
const fieldIndex = state.fields.findIndex(field => {
|
||||
return field.id===id;
|
||||
});
|
||||
state.fields[fieldIndex].help = false;
|
||||
})
|
||||
)
|
||||
},
|
||||
addHelpNotice : (id, label, text, title, url) => {
|
||||
get().removeHelpNotice(id);
|
||||
//create help object
|
||||
|
||||
let help = {};
|
||||
help.label=label;
|
||||
help.text=text;
|
||||
if (url) help.url=url;
|
||||
if (title) help.title=title;
|
||||
set(
|
||||
produce((state) => {
|
||||
const fieldIndex = state.fields.findIndex(field => {
|
||||
return field.id===id;
|
||||
});
|
||||
if (fieldIndex!==-1) {
|
||||
state.fields[fieldIndex].help = help;
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
fieldAlreadyEnabled: (id) => {
|
||||
let fieldIsChanged = get().changedFields.filter(field => field.id === id ).length>0;
|
||||
let fieldIsEnabled = get().getFieldValue(id);
|
||||
return !fieldIsChanged && fieldIsEnabled;
|
||||
},
|
||||
getFieldValue : (id) => {
|
||||
let fields = get().fields;
|
||||
let fieldItem = fields.filter(field => field.id === id )[0];
|
||||
if (fieldItem){
|
||||
return fieldItem.value;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
getField : (id) => {
|
||||
let fields = get().fields;
|
||||
let fieldItem = fields.filter(field => field.id === id )[0];
|
||||
if (fieldItem){
|
||||
return fieldItem;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
saveFields: async (skipRefreshTests, showSavedNotice, force = false) => {
|
||||
let refreshTests = typeof skipRefreshTests !== 'undefined' ? skipRefreshTests : true;
|
||||
showSavedNotice = typeof showSavedNotice !== 'undefined' ? showSavedNotice : true;
|
||||
let fields = get().fields;
|
||||
fields = fields.filter(field => field.data_target !== 'banner');
|
||||
let changedFields = get().changedFields;
|
||||
let saveFields = [];
|
||||
//data_target
|
||||
for (const field of fields) {
|
||||
let fieldIsIncluded = changedFields.filter(changedField => changedField.id === field.id).length > 0;
|
||||
//also check if there's no saved value yet for radio fields, by checking the never_saved attribute.
|
||||
//a radio or select field looks like it's completed, but won't save if it isn't changed.
|
||||
//this should not be the case for disabled fields, as these fields often are enabled server side because they're enabled outside Really Simple Security.
|
||||
let select_or_radio = field.type === 'select' || field.type === 'radio';
|
||||
if (fieldIsIncluded || (field.never_saved && !field.disabled && select_or_radio)) {
|
||||
saveFields.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
//if no fields were changed, do nothing.
|
||||
if (saveFields.length > 0 || force === true) {
|
||||
let response = rsssl_api.setFields(saveFields).then((response) => {
|
||||
return response;
|
||||
})
|
||||
|
||||
if (showSavedNotice) {
|
||||
toast.promise(
|
||||
response,
|
||||
{
|
||||
pending: __('Saving settings...', 'really-simple-ssl'),
|
||||
success: __('Settings saved', 'really-simple-ssl'),
|
||||
error: __('Something went wrong', 'really-simple-ssl'),
|
||||
}
|
||||
);
|
||||
}
|
||||
await response.then((response) => {
|
||||
set(
|
||||
produce((state) => {
|
||||
state.changedFields = [];
|
||||
state.fields = response.fields;
|
||||
state.progress = response.progress;
|
||||
state.refreshTests = refreshTests;
|
||||
})
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
if (showSavedNotice && saveFields.length === 0) {
|
||||
//nothing to save. show instant success.
|
||||
toast.promise(
|
||||
Promise.resolve(),
|
||||
{
|
||||
success: __('Settings saved', 'really-simple-ssl'),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
updateFieldsData: (selectedSubMenuItem) => {
|
||||
let fields = get().fields;
|
||||
fields = updateFieldsListWithConditions(fields);
|
||||
|
||||
//only if selectedSubMenuItem is actually passed
|
||||
if (selectedSubMenuItem) {
|
||||
let nextButtonDisabled = isNextButtonDisabled(fields, selectedSubMenuItem);
|
||||
//if the button was set to disabled with the handleNextButtonDisabled function, we give that priority until it's released.
|
||||
if (get().overrideNextButtonDisabled) {
|
||||
nextButtonDisabled = get().overrideNextButtonDisabled;
|
||||
}
|
||||
set(
|
||||
produce((state) => {
|
||||
state.nextButtonDisabled = nextButtonDisabled;
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
set(
|
||||
produce((state) => {
|
||||
state.fields = fields;
|
||||
})
|
||||
)
|
||||
},
|
||||
fetchFieldsData: async ( selectedSubMenuItem ) => {
|
||||
const { fields, progress, error } = await fetchFields();
|
||||
let conditionallyEnabledFields = updateFieldsListWithConditions(fields);
|
||||
let selectedFields = conditionallyEnabledFields.filter(field => field.menu_id === selectedSubMenuItem);
|
||||
set({fieldsLoaded: true, fields:conditionallyEnabledFields, selectedFields:selectedFields, progress:progress, error: error });
|
||||
}
|
||||
}));
|
||||
|
||||
export default useFields;
|
||||
|
||||
//check if all required fields have been enabled. If so, enable save/continue button
|
||||
const isNextButtonDisabled = (fields, selectedMenuItem) => {
|
||||
let fieldsOnPage = [];
|
||||
//get all fields with group_id this.props.group_id
|
||||
for (const field of fields){
|
||||
if (field.menu_id === selectedMenuItem ){
|
||||
fieldsOnPage.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
let requiredFields = fieldsOnPage.filter(field => field.required && !field.conditionallyDisabled && (field.value.length==0 || !field.value) );
|
||||
return requiredFields.length > 0;
|
||||
}
|
||||
|
||||
const updateFieldsListWithConditions = (fields) => {
|
||||
let newFields = [];
|
||||
if (!fields || !Array.isArray(fields)) {
|
||||
return [];
|
||||
}
|
||||
fields.forEach(function(field, i) {
|
||||
// Create instance to evaluate the react_conditions of a field
|
||||
let reactConditions = new ReactConditions(field, fields);
|
||||
let fieldCurrentlyEnabled = reactConditions.isEnabled();
|
||||
|
||||
//we want to update the changed fields if this field has just become visible. Otherwise the new field won't get saved.
|
||||
const newField = {...field};
|
||||
newField.conditionallyDisabled = (fieldCurrentlyEnabled === false);
|
||||
|
||||
// ReactConditions will keep the original disabledTooltipText if the
|
||||
// field is not disabled by the react_conditions. Key differs from the
|
||||
// one in the config on purpose to prevent overwriting the config value.
|
||||
newField.disabledTooltipHoverText = reactConditions.getDisabledTooltipText();
|
||||
|
||||
// If the field is a letsencrypt field or has a condition_action of
|
||||
// 'hide', it should be hidden if the field is disabled.
|
||||
newField.visible = !(!fieldCurrentlyEnabled && (newField.type === 'letsencrypt' || newField.condition_action === 'hide'));
|
||||
newFields.push(newField);
|
||||
});
|
||||
return newFields;
|
||||
}
|
||||
|
||||
const handleShowSavedSettingsNotice = ( text, type ) => {
|
||||
if (typeof text === 'undefined') {
|
||||
text = __( 'Settings saved', 'really-simple-ssl' );
|
||||
}
|
||||
|
||||
if (typeof type === 'undefined') {
|
||||
type = 'success';
|
||||
}
|
||||
|
||||
if (type === 'error') {
|
||||
toast.error(text);
|
||||
}
|
||||
|
||||
if (type === 'warning') {
|
||||
toast.warning(text);
|
||||
}
|
||||
|
||||
if (type === 'info') {
|
||||
toast.info(text);
|
||||
}
|
||||
|
||||
if (type === 'success') {
|
||||
toast.success(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
import DataTableWrapper from "../DataTable/DataTableWrapper";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import DataTableStore from "../DataTable/DataTableStore";
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import useFields from "../FieldsData";
|
||||
import useMenu from "../../Menu/MenuData";
|
||||
import {toast} from "react-toastify";
|
||||
|
||||
const FileChangeDetection = ({field}) => {
|
||||
const {
|
||||
clearAllData,
|
||||
setProcessing,
|
||||
} = DataTableStore();
|
||||
const { updateFieldsData, showSavedSettingsNotice } = useFields();
|
||||
const { selectedSubMenuItem} = useMenu();
|
||||
const enabled = true;
|
||||
const handleClick = async () => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
'reset_changed_files',
|
||||
{}
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
showSavedSettingsNotice(__('File changes have been been reset', 'really-simple-ssl') );
|
||||
clearAllData();
|
||||
setProcessing(false);
|
||||
//field now should be disabled, as it's now processing
|
||||
updateFieldsData(selectedSubMenuItem);
|
||||
}
|
||||
}
|
||||
|
||||
let controlButton = {
|
||||
show:true,
|
||||
onClick:handleClick,
|
||||
label:__("Reset changed files", "really-simple-ssl")
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTableWrapper
|
||||
field={field}
|
||||
controlButton={controlButton}
|
||||
enabled={true}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileChangeDetection;
|
||||
@@ -0,0 +1,17 @@
|
||||
// FilterData.js
|
||||
import {create} from 'zustand';
|
||||
|
||||
const filterData = create((set, get) => ({
|
||||
selectedFilter: [],
|
||||
processingFilter: false,
|
||||
setSelectedFilter: (selectedFilter, activeGroupId) => {
|
||||
set((state) => ({
|
||||
//we make it an array, so we can have multiple filters
|
||||
selectedFilter: {...state.selectedFilter, [activeGroupId]: selectedFilter},
|
||||
}));
|
||||
},
|
||||
getCurrentFilter: (activeGroupId) => get().selectedFilter[activeGroupId],
|
||||
setProcessingFilter: (processingFilter) => set({processingFilter}),
|
||||
}));
|
||||
|
||||
export default filterData;
|
||||
@@ -0,0 +1,19 @@
|
||||
import Icon from "../../utils/Icon";
|
||||
|
||||
const AddButton = ({ getCurrentFilter, moduleName, handleOpen, processing, blockedText, allowedText, disabled }) => {
|
||||
return (
|
||||
<div className="rsssl-add-button">
|
||||
<div className="rsssl-add-button__inner">
|
||||
<button
|
||||
className="button button-secondary button-datatable rsssl-add-button__button"
|
||||
onClick={handleOpen}
|
||||
disabled={disabled}
|
||||
>
|
||||
{allowedText}{processing && <Icon name = "loading" color = 'grey' />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddButton;
|
||||
@@ -0,0 +1,370 @@
|
||||
import React, { useEffect, useState, useCallback } from '@wordpress/element';
|
||||
import DataTable, {createTheme} from "react-data-table-component";
|
||||
import FieldsData from "../FieldsData";
|
||||
import WhiteListTableStore from "./WhiteListTableStore";
|
||||
import FilterData from "../FilterData";
|
||||
import Flag from "../../utils/Flag/Flag";
|
||||
import {__} from '@wordpress/i18n';
|
||||
import useFields from "../FieldsData";
|
||||
import AddButton from "./AddButton";
|
||||
import TrustIpAddressModal from "./TrustIpAddressModal";
|
||||
|
||||
const BlockListDatatable = (props) => {
|
||||
const {
|
||||
BlockListData,
|
||||
WhiteListTable,
|
||||
setDataLoaded,
|
||||
dataLoaded,
|
||||
dataLoaded_block,
|
||||
fetchData,
|
||||
processing_block,
|
||||
ipAddress,
|
||||
pagination,
|
||||
resetRow,
|
||||
rowCleared,
|
||||
} = WhiteListTableStore();
|
||||
|
||||
const {showSavedSettingsNotice, saveFields} = FieldsData();
|
||||
|
||||
const [rowsSelected, setRowsSelected] = useState([]);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [tableHeight, setTableHeight] = useState(600); // Starting height
|
||||
const rowHeight = 50; // Height of each row.
|
||||
const moduleName = 'rsssl-group-filter-firewall_block_list_listing';
|
||||
const {fields, fieldAlreadyEnabled, getFieldValue} = useFields();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [DataTable, setDataTable] = useState(null);
|
||||
const [theme, setTheme] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
import('react-data-table-component').then((module) => {
|
||||
const { default: DataTable, createTheme } = module;
|
||||
setDataTable(() => DataTable);
|
||||
setTheme(() => createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light'));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
const handlePerRowsChange = (newRowsPerPage) => {
|
||||
setRowsPerPage(newRowsPerPage);
|
||||
};
|
||||
|
||||
const {
|
||||
getCurrentFilter,
|
||||
} = FilterData();
|
||||
|
||||
const [filter, setFilter] = useState(getCurrentFilter(moduleName));
|
||||
|
||||
/**
|
||||
* Build a column configuration object.
|
||||
*
|
||||
* @param {object} column - The column object.
|
||||
* @param {string} column.name - The name of the column.
|
||||
* @param {boolean} column.sortable - Whether the column is sortable.
|
||||
* @param {boolean} column.searchable - Whether the column is searchable.
|
||||
* @param {number} column.width - The width of the column.
|
||||
* @param {boolean} column.visible - Whether the column is visible.
|
||||
* @param {string} column.column - The column identifier.
|
||||
*
|
||||
* @returns {object} The column configuration object.
|
||||
*/
|
||||
const buildColumn = useCallback((column) => ({
|
||||
//if the filter is set to region and the columns = status we do not want to show the column
|
||||
name: column.name,
|
||||
sortable: column.sortable,
|
||||
searchable: column.searchable,
|
||||
width: column.width,
|
||||
visible: column.visible,
|
||||
column: column.column,
|
||||
selector: row => row[column.column],
|
||||
}), [filter]);
|
||||
let field = props.field;
|
||||
const columns = field.columns.map(buildColumn);
|
||||
|
||||
const searchableColumns = columns
|
||||
.filter(column => column.searchable)
|
||||
.map(column => column.column);
|
||||
|
||||
useEffect(() => {
|
||||
setRowsSelected([]);
|
||||
}, [BlockListData]);
|
||||
|
||||
let enabled = getFieldValue('enable_firewall');
|
||||
|
||||
useEffect(() => {
|
||||
const currentFilter = getCurrentFilter(moduleName);
|
||||
if (typeof currentFilter === 'undefined') {
|
||||
setFilter('all');
|
||||
} else {
|
||||
setFilter(currentFilter);
|
||||
}
|
||||
setRowsSelected([]);
|
||||
// resetRowSelection(true);
|
||||
// resetRowSelection(false);
|
||||
}, [getCurrentFilter(moduleName)]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
saveFields(false, false)
|
||||
};
|
||||
}, [enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof filter !== 'undefined') {
|
||||
fetchData(field.action, filter);
|
||||
}
|
||||
}, [filter]);
|
||||
|
||||
useEffect(() => {
|
||||
if(!dataLoaded_block) {
|
||||
fetchData(field.action, filter);
|
||||
}
|
||||
}, [dataLoaded_block]);
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light');
|
||||
|
||||
const handleSelection = useCallback((state) => {
|
||||
//based on the current page and the rows per page we get the rows that are selected
|
||||
const {selectedCount, selectedRows, allSelected, allRowsSelected} = state;
|
||||
let rows = [];
|
||||
if (allSelected) {
|
||||
rows = selectedRows.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage);
|
||||
setRowsSelected(rows);
|
||||
} else {
|
||||
setRowsSelected(selectedRows);
|
||||
}
|
||||
}, [currentPage, rowsPerPage]);
|
||||
|
||||
const allowById = useCallback((id) => {
|
||||
//We check if the id is an array
|
||||
if (Array.isArray(id)) {
|
||||
//We get all the iso2 codes and names from the array
|
||||
const ids = id.map(item => ({
|
||||
id: item.id,
|
||||
}));
|
||||
//we loop through the ids and allow them one by one
|
||||
ids.forEach((id) => {
|
||||
resetRow(id.id).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
});
|
||||
// fetchData(field.action, filter ? filter : 'all');
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
resetRow(id).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
// fetchData(field.action, filter ? filter : 'all');
|
||||
}
|
||||
|
||||
setDataLoaded(false);
|
||||
|
||||
}, [resetRow]);
|
||||
const data = {...BlockListData.data};
|
||||
|
||||
const generateFlag = useCallback((flag, title) => (
|
||||
<>
|
||||
<Flag
|
||||
countryCode={flag}
|
||||
style={{
|
||||
fontSize: '2em',
|
||||
}}
|
||||
title={title}
|
||||
/>
|
||||
</>
|
||||
), []);
|
||||
|
||||
const ActionButton = ({ onClick, children, className }) => (
|
||||
// <div className={`rsssl-action-buttons__inner`}>
|
||||
<button
|
||||
className={`button ${className} rsssl-action-buttons__button`}
|
||||
onClick={onClick}
|
||||
disabled={processing_block}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
// </div>
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
setModalOpen(false);
|
||||
}
|
||||
|
||||
const handleOpen = () => {
|
||||
setModalOpen(true);
|
||||
}
|
||||
|
||||
const generateActionButtons = useCallback((id) => {
|
||||
return (<div className="rsssl-action-buttons">
|
||||
<ActionButton
|
||||
onClick={() => allowById(id)} className="button-red">
|
||||
{__("Reset", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</div>)
|
||||
}, [moduleName, allowById]);
|
||||
|
||||
|
||||
|
||||
for (const key in data) {
|
||||
const dataItem = {...data[key]};
|
||||
dataItem.action = generateActionButtons(dataItem.id);
|
||||
dataItem.flag = generateFlag(dataItem.iso2_code, dataItem.country_name);
|
||||
dataItem.status = __(dataItem.status = dataItem.status.charAt(0).toUpperCase() + dataItem.status.slice(1), 'really-simple-ssl');
|
||||
data[key] = dataItem;
|
||||
}
|
||||
|
||||
let paginationSet;
|
||||
paginationSet = typeof pagination !== 'undefined';
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(data).length === 0 ) {
|
||||
setTableHeight(100); // Adjust depending on your UI measurements
|
||||
} else {
|
||||
setTableHeight(rowHeight * (paginationSet ? pagination.perPage + 2 : 12)); // Adjust depending on your UI measurements
|
||||
}
|
||||
|
||||
}, [paginationSet, pagination?.perPage, data]);
|
||||
|
||||
useEffect(() => {
|
||||
let intervals = [];
|
||||
const filteredData = Object.entries(data)
|
||||
.filter(([_, dataItem]) => {
|
||||
return Object.values(dataItem).some(val => ((val ?? '').toString().toLowerCase().includes(searchTerm.toLowerCase())));
|
||||
})
|
||||
.map(([key, dataItem]) => {
|
||||
const newItem = { ...dataItem };
|
||||
newItem.action = generateActionButtons(newItem.id);
|
||||
newItem.flag = generateFlag(newItem.iso2_code, newItem.country_name);
|
||||
newItem.status = __(newItem.status = newItem.status.charAt(0).toUpperCase() + newItem.status.slice(1), 'really-simple-ssl');
|
||||
// if the newItem.time_left not is 0 we count down in seconds the value
|
||||
if (newItem.time_left > 0) {
|
||||
const interval = setInterval(() => {
|
||||
newItem.time_left--;
|
||||
}, 1000);
|
||||
intervals.push(interval);
|
||||
}
|
||||
return [key, newItem];
|
||||
})
|
||||
.reduce((obj, [key, val]) => ({ ...obj, [key]: val }), {});
|
||||
}, [searchTerm, data]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<TrustIpAddressModal
|
||||
isOpen={modalOpen}
|
||||
onRequestClose={handleClose}
|
||||
value={ipAddress}
|
||||
status={'blocked'}
|
||||
filter={filter? filter : 'all'}
|
||||
>
|
||||
</TrustIpAddressModal>
|
||||
<div className="rsssl-container">
|
||||
{/*display the add button on left side*/}
|
||||
<AddButton
|
||||
moduleName={moduleName}
|
||||
handleOpen={handleOpen}
|
||||
processing={processing_block}
|
||||
allowedText={__("Block IP Address", "really-simple-ssl")}
|
||||
/>
|
||||
<div className="rsssl-search-bar">
|
||||
<div className="rsssl-search-bar__inner">
|
||||
<div className="rsssl-search-bar__icon"></div>
|
||||
<input
|
||||
type="text"
|
||||
className="rsssl-search-bar__input"
|
||||
placeholder={__("Search", "really-simple-ssl")}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{rowsSelected.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
}}
|
||||
>
|
||||
<div className={"rsssl-multiselect-datatable-form rsssl-primary"}>
|
||||
<div>
|
||||
{__("You have selected %s rows", "really-simple-ssl").replace('%s', rowsSelected.length)}
|
||||
</div>
|
||||
<div className="rsssl-action-buttons">
|
||||
<>
|
||||
<ActionButton
|
||||
onClick={() => allowById(rowsSelected)} className="button-red">
|
||||
{__("Reset", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{DataTable &&
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={Object.values(data).filter((row) => {
|
||||
return Object.values(row).some((val) => ((val ?? '').toString().toLowerCase()).includes(searchTerm.toLowerCase()));
|
||||
})}
|
||||
dense
|
||||
pagination={true}
|
||||
paginationComponentOptions={{
|
||||
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
|
||||
rangeSeparatorText: __('of', 'really-simple-ssl'),
|
||||
noRowsPerPage: false,
|
||||
selectAllRowsItem: false,
|
||||
selectAllRowsItemText: __('All', 'really-simple-ssl'),
|
||||
|
||||
}}
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
selectableRows={true}
|
||||
clearSelectedRows={rowCleared}
|
||||
paginationPerPage={rowsPerPage}
|
||||
onChangePage={handlePageChange}
|
||||
onChangeRowsPerPage={handlePerRowsChange}
|
||||
onSelectedRowsChange={handleSelection}
|
||||
theme="really-simple-plugins"
|
||||
customStyles={customStyles}
|
||||
/>}
|
||||
{!enabled && (
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay"><span
|
||||
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Here you can add IP addresses that should never be blocked by region restrictions.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlockListDatatable;
|
||||
@@ -0,0 +1,316 @@
|
||||
/* Creates A Store For Risk Data using Zustand */
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import {produce} from "immer";
|
||||
import React from "react";
|
||||
import GeoDatatable from "./GeoDatatable";
|
||||
|
||||
const GeoDataTableStore = create((set, get) => ({
|
||||
|
||||
processing: false,
|
||||
dataLoaded: false,
|
||||
pagination: {},
|
||||
dataActions: {},
|
||||
CountryDataTable: [],
|
||||
rowCleared: false,
|
||||
|
||||
fetchCountryData: async (action, filterValue) => {
|
||||
//we check if the processing is already true, if so we return
|
||||
set({
|
||||
processing: true,
|
||||
rowCleared: true,
|
||||
dataLoaded: false
|
||||
});
|
||||
// if the filterValue is not set, we do nothing.
|
||||
if (!filterValue) {
|
||||
set({processing: false});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action, {filterValue}
|
||||
);
|
||||
//now we set the EventLog
|
||||
if (response && response.request_success) {
|
||||
set({
|
||||
CountryDataTable: response,
|
||||
dataLoaded: true,
|
||||
processing: false
|
||||
});
|
||||
}
|
||||
set({ rowCleared: true });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
|
||||
handleCountryTableSearch: async (search, searchColumns) => {
|
||||
//Add the search to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, search, searchColumns};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
handleCountryTablePageChange: async (page, pageSize) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, page, pageSize};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
handleCountryTableRowsChange: async (currentRowsPerPage, currentPage) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, currentRowsPerPage, currentPage};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
//this handles all pagination and sorting
|
||||
handleCountryTableSort: async (column, sortDirection) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, sortColumn: column, sortDirection};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
handleCountryTableFilter: async (column, filterValue) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, filterColumn: column, filterValue};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
/*
|
||||
* This function add a new row to the table
|
||||
*/
|
||||
addRow: async (country, name) => {
|
||||
set({rowCleared: false});
|
||||
let data = {
|
||||
country_code: country,
|
||||
country_name: name
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_add_blocked_country', data);
|
||||
if (response && response.request_success) {
|
||||
set({rowCleared: true});
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Return a custom error message or the API response message.
|
||||
return { success: false, message: response?.message || 'Failed to add country', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Return the caught error with a custom message.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
}
|
||||
},
|
||||
|
||||
addMultiRow: async (countries) => {
|
||||
set({processing: true});
|
||||
set({rowCleared: false});
|
||||
let data = {
|
||||
country_codes: countries
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_add_blocked_country', data);
|
||||
if (response && response.request_success) {
|
||||
set({rowCleared: true});
|
||||
// Return the success message from the API response.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
set({rowCleared: true});
|
||||
// Return a custom error message or the API response message.
|
||||
return { success: false, message: response?.message || 'Failed to add countries', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
set({rowCleared: true});
|
||||
// Return the caught error with a custom message.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
set({rowCleared: true});
|
||||
}
|
||||
},
|
||||
|
||||
removeRowMulti: async (countries, dataActions) => {
|
||||
set({processing: true});
|
||||
set({rowCleared: false});
|
||||
let data = {
|
||||
country_codes: countries
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_remove_blocked_country', data);
|
||||
if (response && response.request_success) {
|
||||
set({rowCleared: true});
|
||||
// Return the success message from the API response.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Return a custom error message or the API response message.
|
||||
return { success: false, message: response?.message || 'Failed to remove countries', response };
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
set({rowCleared: true});
|
||||
// Return the caught error with a custom message.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({rowCleared: true});
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
|
||||
removeRow: async (country) => {
|
||||
set({processing: true});
|
||||
set({rowCleared: false});
|
||||
let data = {
|
||||
country_code: country
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_remove_blocked_country', data);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
set({rowCleared: true});
|
||||
// Potentially notify the user of success, if needed.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Handle any unsuccessful response if needed.
|
||||
set({rowCleared: true});
|
||||
return { success: false, message: response?.message || 'Failed to remove country', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Notify the user of an error.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({rowCleared: true});
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
|
||||
addRegion: async (region) => {
|
||||
set({processing: true});
|
||||
set({rowCleared: false});
|
||||
let data = {
|
||||
region_code: region
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_add_blocked_region', data);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
set({rowCleared: true});
|
||||
// Potentially notify the user of success, if needed.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Handle any unsuccessful response if needed.
|
||||
set({rowCleared: true});
|
||||
return { success: false, message: response?.message || 'Failed to add region', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Notify the user of an error.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
set({rowCleared: true});
|
||||
}
|
||||
},
|
||||
|
||||
addRegionsMulti: async (regions, dataActions) => {
|
||||
set({processing: true});
|
||||
set({rowCleared: false});
|
||||
let data = {
|
||||
region_codes: regions
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_add_blocked_region', data);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
set({rowCleared: true});
|
||||
// Potentially notify the user of success, if needed.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
set({rowCleared: true});
|
||||
// Handle any unsuccessful response if needed.
|
||||
return { success: false, message: response?.message || 'Failed to add regions', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Notify the user of an error.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({rowCleared: true});
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
|
||||
removeRegion: async (region) => {
|
||||
set({processing: true});
|
||||
set({rowCleared: false});
|
||||
let data = {
|
||||
region_code: region
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_remove_blocked_region', data);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
set({rowCleared: true});
|
||||
// Potentially notify the user of success, if needed.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Handle any unsuccessful response if needed.
|
||||
set({rowCleared: true});
|
||||
return { success: false, message: response?.message || 'Failed to remove region', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Notify the user of an error.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
set({rowCleared: true});
|
||||
}
|
||||
},
|
||||
|
||||
removeRegionMulti: async (regions) => {
|
||||
set({processing: true});
|
||||
set({rowCleared: false});
|
||||
let data = {
|
||||
region_codes: regions
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_remove_blocked_region', data);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
// Potentially notify the user of success, if needed.
|
||||
set({rowCleared: true});
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
set({rowCleared: true});
|
||||
// Handle any unsuccessful response if needed.
|
||||
return { success: false, message: response?.message || 'Failed to remove regions', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Notify the user of an error.
|
||||
set({rowCleared: true});
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
set({rowCleared: true});
|
||||
}
|
||||
},
|
||||
|
||||
resetRowSelection: async (on_off) => {
|
||||
set({rowCleared: on_off});
|
||||
}
|
||||
}));
|
||||
|
||||
export default GeoDataTableStore;
|
||||
@@ -0,0 +1,528 @@
|
||||
import {useEffect, useState, useCallback} from '@wordpress/element';
|
||||
import FieldsData from "../FieldsData";
|
||||
import GeoDataTableStore from "./GeoDataTableStore";
|
||||
import EventLogDataTableStore from "../EventLog/EventLogDataTableStore";
|
||||
import FilterData from "../FilterData";
|
||||
import Flag from "../../utils/Flag/Flag";
|
||||
import {__} from '@wordpress/i18n';
|
||||
import useFields from "../FieldsData";
|
||||
import useMenu from "../../Menu/MenuData";
|
||||
|
||||
/**
|
||||
* A component for displaying a geo datatable.
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string} props.field - The field to display.
|
||||
*
|
||||
* @returns {JSX.Element} The rendered component.
|
||||
*/
|
||||
const GeoDatatable = (props) => {
|
||||
const {
|
||||
CountryDataTable,
|
||||
dataLoaded,
|
||||
fetchCountryData,
|
||||
addRow,
|
||||
addMultiRow,
|
||||
removeRegion,
|
||||
removeRegionMulti,
|
||||
addRegion,
|
||||
addRegionsMulti,
|
||||
removeRow,
|
||||
removeRowMulti,
|
||||
rowCleared,
|
||||
resetRowSelection,
|
||||
} = GeoDataTableStore();
|
||||
|
||||
const moduleName = 'rsssl-group-filter-firewall_list_listing';
|
||||
const [localData, setLocalData] = useState([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [visualData, setVisualData] = useState([]);
|
||||
const {showSavedSettingsNotice, saveFields} = FieldsData();
|
||||
const [rowsSelected, setRowsSelected] = useState([]);
|
||||
const [columns, setColumns] = useState([]);
|
||||
const {fields, fieldAlreadyEnabled, getFieldValue, setHighLightField, getField} = useFields();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const {setSelectedSubMenuItem} = useMenu();
|
||||
const [DataTable, setDataTable] = useState(null);
|
||||
const [theme, setTheme] = useState(null);
|
||||
|
||||
useEffect( () => {
|
||||
import('react-data-table-component').then(({ default: DataTable, createTheme }) => {
|
||||
setDataTable(() => DataTable);
|
||||
setTheme(() => createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light'));
|
||||
});
|
||||
|
||||
}, []);
|
||||
|
||||
let enabled = getFieldValue('enable_firewall');
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
const handlePerRowsChange = (newRowsPerPage) => {
|
||||
setRowsPerPage(newRowsPerPage);
|
||||
};
|
||||
|
||||
const {
|
||||
selectedFilter,
|
||||
setSelectedFilter,
|
||||
activeGroupId,
|
||||
getCurrentFilter,
|
||||
setProcessingFilter,
|
||||
} = FilterData();
|
||||
|
||||
const [filter, setFilter] = useState(getCurrentFilter(moduleName));
|
||||
|
||||
const buildColumn = useCallback((column) => ({
|
||||
//if the filter is set to region and the columns = status we do not want to show the column
|
||||
omit: filter === 'regions' && (column.column === 'country_name' || column.column === 'flag'),
|
||||
name: (column.column === 'action' && 'regions' === filter) ? __('Block / Allow All', 'really-simple-ssl') : column.name,
|
||||
sortable: column.sortable,
|
||||
searchable: column.searchable,
|
||||
width: column.width,
|
||||
visible: column.visible,
|
||||
column: column.column,
|
||||
selector: row => row[column.column],
|
||||
}), [filter]);
|
||||
let field = props.field;
|
||||
|
||||
useEffect(() => {
|
||||
const element = document.getElementById('set_to_captcha_configuration');
|
||||
const clickListener = async event => {
|
||||
event.preventDefault();
|
||||
if (element) {
|
||||
await redirectToAddCaptcha(element);
|
||||
}
|
||||
};
|
||||
|
||||
if (element) {
|
||||
element.addEventListener('click', clickListener);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (element) {
|
||||
element.removeEventListener('click', clickListener);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const redirectToAddCaptcha = async (element) => {
|
||||
// We fetch the props from the menu item
|
||||
let menuItem = getField('enabled_captcha_provider');
|
||||
|
||||
// Create a new object based on the menuItem, including the new property
|
||||
let highlightingMenuItem = {
|
||||
...menuItem,
|
||||
highlight_field_id: 'enabled_captcha_provider',
|
||||
};
|
||||
|
||||
setHighLightField(highlightingMenuItem.highlight_field_id);
|
||||
let highlightField = getField(highlightingMenuItem.highlight_field_id);
|
||||
await setSelectedSubMenuItem(highlightField.menu_id);
|
||||
}
|
||||
|
||||
const blockCountryByCode = useCallback(async (code, name) => {
|
||||
if (Array.isArray(code)) {
|
||||
//We get all the iso2 codes and names from the array
|
||||
const ids = code.map(item => ({
|
||||
country_code: item.iso2_code,
|
||||
country_name: item.country_name
|
||||
}));
|
||||
//we loop through the ids and block them one by one
|
||||
await addMultiRow(ids).then(
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
}
|
||||
);
|
||||
await fetchCountryData(field.action, filter);
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
await addRow(code, name).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
if (result.success) {
|
||||
fetchCountryData(field.action, filter);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [addRow, filter, localData, enabled]);
|
||||
|
||||
const allowRegionByCode = useCallback(async (code, regionName = '') => {
|
||||
if (Array.isArray(code)) {
|
||||
const ids = code.map(item => ({
|
||||
iso2_code: item.iso2_code,
|
||||
country_name: item.country_name
|
||||
}));
|
||||
await removeRegionMulti(ids).then(
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
if (response.success) {
|
||||
fetchCountryData(field.action, filter);
|
||||
}
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
}
|
||||
);
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
await removeRegion(code).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
if (result.success) {
|
||||
fetchCountryData(field.action, filter);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [removeRegion, filter]);
|
||||
|
||||
const blockRegionByCode = useCallback(async (code, region = '') => {
|
||||
if (Array.isArray(code)) {
|
||||
const ids = code.map(item => ({
|
||||
iso2_code: item.iso2_code,
|
||||
country_name: item.country_name
|
||||
}));
|
||||
await addRegionsMulti(ids).then(
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
}
|
||||
);
|
||||
await fetchCountryData(field.action, filter);
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
await addRegion(code).then((result) => {
|
||||
if (result.success) {
|
||||
showSavedSettingsNotice(result.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(result.message, 'error');
|
||||
}
|
||||
});
|
||||
await fetchCountryData(field.action, filter);
|
||||
}
|
||||
|
||||
}, [addRegion, filter]);
|
||||
|
||||
const allowCountryByCode = useCallback(async (code) => {
|
||||
if (Array.isArray(code)) {
|
||||
const ids = code.map(item => ({
|
||||
country_code: item.iso2_code,
|
||||
country_name: item.country_name
|
||||
}));
|
||||
//we loop through the ids and allow them one by one
|
||||
await removeRowMulti(ids).then(
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
setRowsSelected([]);
|
||||
await fetchCountryData(field.action, filter);
|
||||
} else {
|
||||
await removeRow(code).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
await fetchCountryData(field.action, filter);
|
||||
}
|
||||
|
||||
}, [removeRow, filter]);
|
||||
|
||||
const ActionButton = ({onClick, children, className, disabled = false}) => (
|
||||
// <div className={`rsssl-action-buttons__inner`}>
|
||||
<button
|
||||
className={`button ${className} rsssl-action-buttons__button`}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
// </div>
|
||||
);
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const generateActionButtons = useCallback((code, name, region_name, showBlockButton = true, showAllowButton = true) => {
|
||||
return (<div className="rsssl-action-buttons">
|
||||
{filter === 'blocked' && (
|
||||
<ActionButton
|
||||
onClick={() => allowCountryByCode(code)}
|
||||
className="button-secondary">
|
||||
{__("Allow", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
)}
|
||||
{filter === 'regions' && (
|
||||
<>
|
||||
<ActionButton
|
||||
onClick={() => blockRegionByCode(code, region_name)}
|
||||
className="button-primary"
|
||||
disabled={!showBlockButton}
|
||||
>
|
||||
{__("Block", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={() => allowRegionByCode(code, region_name)}
|
||||
className="button-secondary"
|
||||
disabled={!showAllowButton}
|
||||
>
|
||||
{__("Allow", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</>
|
||||
)}
|
||||
{filter === 'countries' && (
|
||||
<ActionButton
|
||||
onClick={() => blockCountryByCode(code, name)}
|
||||
className="button-primary">
|
||||
{__("Block", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>)
|
||||
}, [filter]);
|
||||
|
||||
const generateFlag = useCallback((flag, title) => {
|
||||
return (
|
||||
<>
|
||||
<Flag
|
||||
countryCode={flag}
|
||||
style={{
|
||||
fontSize: '2em',
|
||||
}}
|
||||
title={title}
|
||||
continent={(getCurrentFilter(moduleName) === 'regions')}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}, [filter]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const currentFilter = getCurrentFilter(moduleName);
|
||||
if (typeof currentFilter === 'undefined') {
|
||||
setFilter('regions');
|
||||
setSelectedFilter('regions', moduleName);
|
||||
} else {
|
||||
setFilter(currentFilter);
|
||||
}
|
||||
setRowsSelected([]);
|
||||
resetRowSelection(true);
|
||||
resetRowSelection(false);
|
||||
}, [getCurrentFilter(moduleName)]);
|
||||
|
||||
useEffect(() => {
|
||||
if (filter !== undefined) {
|
||||
const fetchData = async () => {
|
||||
await fetchCountryData(field.action, filter);
|
||||
}
|
||||
fetchData();
|
||||
}
|
||||
|
||||
}, [filter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dataLoaded && CountryDataTable.data !== undefined) {
|
||||
setLocalData(CountryDataTable.data);
|
||||
}
|
||||
}, [dataLoaded]);
|
||||
|
||||
const handleSelection = useCallback((state) => {
|
||||
//based on the current page and the rows per page we get the rows that are selected
|
||||
const {selectedCount, selectedRows, allSelected, allRowsSelected} = state;
|
||||
let rows = [];
|
||||
if (allSelected) {
|
||||
rows = selectedRows.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage);
|
||||
setRowsSelected(rows);
|
||||
} else {
|
||||
setRowsSelected(selectedRows);
|
||||
}
|
||||
}, [currentPage, rowsPerPage, visualData]);
|
||||
|
||||
useEffect(() => {
|
||||
let FilterColumns = field.columns.map(buildColumn);
|
||||
// Find the index of the 'action' column
|
||||
const actionIndex = FilterColumns.findIndex(column => column.column === 'action');
|
||||
|
||||
|
||||
// If 'filter' equals 'regions' and 'action' column exists, then do the rearrangement
|
||||
if (filter === 'regions' && actionIndex !== -1) {
|
||||
const actionColumn = FilterColumns[actionIndex];
|
||||
|
||||
// Remove 'action' column from its current place
|
||||
FilterColumns.splice(actionIndex, 1);
|
||||
const emptyColumn = {
|
||||
name: '',
|
||||
selector: '',
|
||||
sortable: false,
|
||||
omit: false,
|
||||
searchable: false,
|
||||
};
|
||||
// Push 'action' column to the end of the array
|
||||
FilterColumns.push(emptyColumn, actionColumn);
|
||||
}
|
||||
setColumns(FilterColumns);
|
||||
const generatedVisualData = (localData || [])
|
||||
.filter((row) => {
|
||||
return Object.values(row).some((val) => ((val ?? '').toString().toLowerCase()).includes(searchTerm.toLowerCase()));
|
||||
}).map((row) => {
|
||||
const newRow = {...row};
|
||||
columns.forEach((column) => {
|
||||
newRow[column.column] = row[column.column];
|
||||
});
|
||||
if (filter === 'regions') {
|
||||
let showBlockButton = (newRow.region_count - newRow.blocked_count) > 0;
|
||||
let showAllowButton = (newRow.blocked_count > 0);
|
||||
newRow.action = generateActionButtons(newRow.iso2_code, newRow.country_name, newRow.region, showBlockButton, showAllowButton);
|
||||
} else if (filter === 'countries') {
|
||||
newRow.action = generateActionButtons(newRow.iso2_code, newRow.country_name, newRow.region);
|
||||
} else {
|
||||
newRow.action = generateActionButtons(newRow.iso2_code, newRow.status, newRow.region);
|
||||
}
|
||||
newRow.flag = generateFlag(newRow.iso2_code, newRow.country_name);
|
||||
if (newRow.status) {
|
||||
newRow.status = __(newRow.status.charAt(0).toUpperCase() + newRow.status.slice(1), 'really-simple-ssl');
|
||||
if ('regions' === filter) {
|
||||
// So i all is blocked we don't want to show the count also if all are allowed we don't want to show the count
|
||||
if (newRow.blocked_count === newRow.region_count || newRow.blocked_count === 0) {
|
||||
newRow.status = newRow.status;
|
||||
|
||||
} else {
|
||||
newRow.status = newRow.status + ' (' + newRow.blocked_count + '/ ' + newRow.region_count + ')';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return newRow;
|
||||
});
|
||||
setVisualData(generatedVisualData);
|
||||
}, [localData, searchTerm]);
|
||||
|
||||
useEffect(() => {
|
||||
if ( rowsSelected.length === 0 ) {
|
||||
resetRowSelection
|
||||
}
|
||||
}, [rowsSelected]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-container">
|
||||
<div>
|
||||
{/* reserved for left side buttons */}
|
||||
</div>
|
||||
<div className="rsssl-search-bar">
|
||||
<div className="rsssl-search-bar__inner">
|
||||
<div className="rsssl-search-bar__icon"></div>
|
||||
<input
|
||||
type="text"
|
||||
className="rsssl-search-bar__input"
|
||||
placeholder={__("Search", "really-simple-ssl")}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{rowsSelected.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
}}
|
||||
>
|
||||
<div className={"rsssl-multiselect-datatable-form rsssl-primary"}>
|
||||
<div>
|
||||
{__("You have selected %s rows", "really-simple-ssl").replace('%s', rowsSelected.length)}
|
||||
</div>
|
||||
<div className="rsssl-action-buttons">
|
||||
{filter === 'countries' && (
|
||||
<>
|
||||
<ActionButton
|
||||
onClick={() => blockCountryByCode(rowsSelected)} className="button-primary">
|
||||
{__("Block", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</>
|
||||
)}
|
||||
{filter === 'regions' && (
|
||||
<>
|
||||
<ActionButton
|
||||
onClick={() => allowRegionByCode(rowsSelected)} className="button-secondary">
|
||||
{__("Allow", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={() => blockRegionByCode(rowsSelected)} className="button-primary">
|
||||
{__("Block", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</>
|
||||
)}
|
||||
{filter === 'blocked' && (
|
||||
<ActionButton
|
||||
onClick={() => allowCountryByCode(rowsSelected)}>
|
||||
{__("Allow", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{DataTable &&
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={visualData}
|
||||
dense
|
||||
pagination={true}
|
||||
paginationComponentOptions={{
|
||||
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
|
||||
rangeSeparatorText: __('of', 'really-simple-ssl'),
|
||||
noRowsPerPage: false,
|
||||
selectAllRowsItem: false,
|
||||
selectAllRowsItemText: __('All', 'really-simple-ssl'),
|
||||
|
||||
}}
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
selectableRows={true}
|
||||
paginationPerPage={rowsPerPage}
|
||||
onChangePage={handlePageChange}
|
||||
onChangeRowsPerPage={handlePerRowsChange}
|
||||
onSelectedRowsChange={handleSelection}
|
||||
clearSelectedRows={rowCleared}
|
||||
theme="really-simple-plugins"
|
||||
customStyles={customStyles}
|
||||
>
|
||||
</DataTable> }
|
||||
{!getFieldValue('enable_firewall') && (
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay"><span
|
||||
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Restrict access from specific countries or continents. You can also allow only specific countries.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GeoDatatable;
|
||||
@@ -0,0 +1,156 @@
|
||||
import {useState} from '@wordpress/element';
|
||||
import Icon from "../../utils/Icon";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
TextControl
|
||||
} from "@wordpress/components";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import FieldsData from "../FieldsData";
|
||||
import WhiteListTableStore from "./WhiteListTableStore";
|
||||
|
||||
const TrustIpAddressModal = (props) => {
|
||||
const { note, setNote, ipAddress, setIpAddress, maskError, setDataLoaded, dataLoaded, updateRow, resetRange} = WhiteListTableStore();
|
||||
const [rangeDisplay, setRangeDisplay] = useState(false);
|
||||
const {showSavedSettingsNotice} = FieldsData();
|
||||
|
||||
//we add a function to handle the range fill
|
||||
const handleRangeFill = () => {
|
||||
//we toggle the range display.
|
||||
setRangeDisplay(!rangeDisplay);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
// we check if statusSelected is not empty
|
||||
if (ipAddress && maskError === false) {
|
||||
await updateRow(ipAddress, note, props.status ,props.filter).then((response) => {
|
||||
if (response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
//we fetch the data again
|
||||
setDataLoaded(false);
|
||||
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
});
|
||||
//we clear the input
|
||||
resetRange();
|
||||
//we close the modal
|
||||
props.onRequestClose();
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
// Reset all local state
|
||||
setRangeDisplay(false);
|
||||
resetRange();
|
||||
|
||||
// Close the modal
|
||||
props.onRequestClose();
|
||||
}
|
||||
if (!props.isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const changeHandler = (e) => {
|
||||
if (e.length > 0) {
|
||||
setIpAddress(e);
|
||||
} else {
|
||||
resetRange()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={__("Add IP Address", "really-simple-ssl")}
|
||||
shouldCloseOnClickOutside={true}
|
||||
shouldCloseOnEsc={true}
|
||||
overlayClassName="rsssl-modal-overlay"
|
||||
className="rsssl-modal"
|
||||
onRequestClose={props.onRequestClose}
|
||||
>
|
||||
<div className="modal-content">
|
||||
<div className="modal-body"
|
||||
style={{
|
||||
padding: "0.5em",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "95%",
|
||||
height: "100%",
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
<div style={{position: 'relative'}}>
|
||||
<label
|
||||
htmlFor={'ip-address'}
|
||||
className={'rsssl-label'}
|
||||
>{__('IP Address', 'really-simple-ssl')}</label>
|
||||
<TextControl
|
||||
id="ip-address"
|
||||
name="ip-address"
|
||||
onChange={changeHandler}
|
||||
value={ipAddress}
|
||||
/>
|
||||
<div className="rsssl-ip-verified">
|
||||
{Boolean(!maskError && ipAddress.length > 0)
|
||||
? <Icon name='circle-check' color={'green'}/>
|
||||
: <Icon name='circle-times' color={'red'}/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor={'note'}
|
||||
className={'rsssl-label'}
|
||||
>{__('Notes', 'really-simple-ssl')}</label>
|
||||
<input
|
||||
name={'note'}
|
||||
id={'note'}
|
||||
type={'text'}
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
{/*//we add two buttons here for add row and cancel*/}
|
||||
<div
|
||||
className={'rsssl-grid-item-footer'}
|
||||
style
|
||||
={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
padding: '1em',
|
||||
}
|
||||
}
|
||||
>
|
||||
<Button
|
||||
isSecondary
|
||||
onClick={handleCancel}
|
||||
style={{ marginRight: '10px' }}
|
||||
|
||||
>
|
||||
{__("Cancel", "really-simple-ssl")}
|
||||
</Button>
|
||||
<Button
|
||||
isPrimary
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{__("Add", "really-simple-ssl")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default TrustIpAddressModal;
|
||||
@@ -0,0 +1,423 @@
|
||||
import { useEffect, useState, useCallback } from '@wordpress/element';
|
||||
import DataTable, {createTheme} from "react-data-table-component";
|
||||
import FieldsData from "../FieldsData";
|
||||
import WhiteListTableStore from "./WhiteListTableStore";
|
||||
import Flag from "../../utils/Flag/Flag";
|
||||
import {__} from '@wordpress/i18n';
|
||||
import useFields from "../FieldsData";
|
||||
import AddButton from "./AddButton";
|
||||
import TrustIpAddressModal from "./TrustIpAddressModal";
|
||||
|
||||
const WhiteListDatatable = (props) => {
|
||||
const {
|
||||
WhiteListTable,
|
||||
fetchWhiteListData,
|
||||
processing,
|
||||
ipAddress,
|
||||
addRow,
|
||||
removeRow,
|
||||
pagination,
|
||||
addRegion,
|
||||
removeRegion,
|
||||
resetRow,
|
||||
rowCleared,
|
||||
} = WhiteListTableStore();
|
||||
|
||||
const {showSavedSettingsNotice, saveFields} = FieldsData();
|
||||
|
||||
const [rowsSelected, setRowsSelected] = useState([]);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [tableHeight, setTableHeight] = useState(600); // Starting height
|
||||
const rowHeight = 50; // Height of each row.
|
||||
const moduleName = 'rsssl-group-filter-geo_block_list_white_listing';
|
||||
const {fields, fieldAlreadyEnabled, getFieldValue} = useFields();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [DataTable, setDataTable] = useState(null);
|
||||
const [theme, setTheme] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
import('react-data-table-component').then((module) => {
|
||||
const { default: DataTable, createTheme } = module;
|
||||
setDataTable(() => DataTable);
|
||||
setTheme(() => createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light'));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
const handlePerRowsChange = (newRowsPerPage) => {
|
||||
setRowsPerPage(newRowsPerPage);
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a column configuration object.
|
||||
*
|
||||
* @param {object} column - The column object.
|
||||
* @param {string} column.name - The name of the column.
|
||||
* @param {boolean} column.sortable - Whether the column is sortable.
|
||||
* @param {boolean} column.searchable - Whether the column is searchable.
|
||||
* @param {number} column.width - The width of the column.
|
||||
* @param {boolean} column.visible - Whether the column is visible.
|
||||
* @param {string} column.column - The column identifier.
|
||||
*
|
||||
* @returns {object} The column configuration object.
|
||||
*/
|
||||
const buildColumn = useCallback((column) => ({
|
||||
//if the filter is set to region and the columns = status we do not want to show the column
|
||||
name: column.name,
|
||||
sortable: column.sortable,
|
||||
searchable: column.searchable,
|
||||
width: column.width,
|
||||
visible: column.visible,
|
||||
column: column.column,
|
||||
selector: row => row[column.column],
|
||||
}), []);
|
||||
let field = props.field;
|
||||
const columns = field.columns.map(buildColumn);
|
||||
|
||||
const searchableColumns = columns
|
||||
.filter(column => column.searchable)
|
||||
.map(column => column.column);
|
||||
|
||||
useEffect(() => {
|
||||
setRowsSelected([]);
|
||||
}, [WhiteListTable]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchWhiteListData(field.action);
|
||||
|
||||
}, [fieldAlreadyEnabled('enable_firewall')]);
|
||||
|
||||
let enabled = getFieldValue('enable_firewall');
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
saveFields(false, false)
|
||||
};
|
||||
}, [enabled]);
|
||||
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light');
|
||||
|
||||
const handleSelection = useCallback((state) => {
|
||||
//based on the current page and the rows per page we get the rows that are selected
|
||||
const {selectedCount, selectedRows, allSelected, allRowsSelected} = state;
|
||||
let rows = [];
|
||||
if (allSelected) {
|
||||
rows = selectedRows.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage);
|
||||
setRowsSelected(rows);
|
||||
} else {
|
||||
setRowsSelected(selectedRows);
|
||||
}
|
||||
}, [currentPage, rowsPerPage]);
|
||||
|
||||
const allowRegionByCode = useCallback(async (code, regionName = '') => {
|
||||
if (Array.isArray(code)) {
|
||||
const ids = code.map(item => ({
|
||||
iso2_code: item.iso2_code,
|
||||
country_name: item.country_name
|
||||
}));
|
||||
ids.forEach((id) => {
|
||||
removeRegion(id.iso2_code).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
});
|
||||
setRowsSelected([]);
|
||||
await fetchWhiteListData(field.action);
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
await removeRegion(code).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
}
|
||||
}, [removeRegion]);
|
||||
|
||||
const allowById = useCallback((id) => {
|
||||
//We check if the id is an array
|
||||
if (Array.isArray(id)) {
|
||||
//We get all the iso2 codes and names from the array
|
||||
const ids = id.map(item => ({
|
||||
id: item.id,
|
||||
}));
|
||||
//we loop through the ids and allow them one by one
|
||||
ids.forEach((id) => {
|
||||
resetRow(id.id).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
});
|
||||
fetchWhiteListData(field.action);
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
resetRow(id).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
fetchWhiteListData(field.action);
|
||||
});
|
||||
}
|
||||
}, [resetRow]);
|
||||
|
||||
const blockRegionByCode = useCallback(async (code, region = '') => {
|
||||
if (Array.isArray(code)) {
|
||||
const ids = code.map(item => ({
|
||||
iso2_code: item.iso2_code,
|
||||
country_name: item.country_name
|
||||
}));
|
||||
ids.forEach((id) => {
|
||||
addRegion(id.iso2_code).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
});
|
||||
setRowsSelected([]);
|
||||
await fetchWhiteListData(field.action);
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
await addRegion(code).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
}
|
||||
|
||||
}, [addRegion]);
|
||||
|
||||
const allowCountryByCode = useCallback(async (code) => {
|
||||
if (Array.isArray(code)) {
|
||||
const ids = code.map(item => ({
|
||||
iso2_code: item.iso2_code,
|
||||
country_name: item.country_name
|
||||
}));
|
||||
//we loop through the ids and allow them one by one
|
||||
ids.forEach((id) => {
|
||||
removeRow(id.iso2_code).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
});
|
||||
setRowsSelected([]);
|
||||
await fetchWhiteListData(field.action);
|
||||
} else {
|
||||
await removeRow(code).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
}
|
||||
|
||||
}, [removeRow]);
|
||||
|
||||
const blockCountryByCode = useCallback(async (code, name) => {
|
||||
if (Array.isArray(code)) {
|
||||
//We get all the iso2 codes and names from the array
|
||||
const ids = code.map(item => ({
|
||||
iso2_code: item.iso2_code,
|
||||
country_name: item.country_name
|
||||
}));
|
||||
//we loop through the ids and block them one by one
|
||||
ids.forEach((id) => {
|
||||
addRow(id.iso2_code, id.country_name).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
});
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
await addRow(code, name).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
}
|
||||
|
||||
}, [addRow]);
|
||||
|
||||
const data = {...WhiteListTable.data};
|
||||
|
||||
const generateFlag = useCallback((flag, title) => (
|
||||
<>
|
||||
<Flag
|
||||
countryCode={flag}
|
||||
style={{
|
||||
fontSize: '2em',
|
||||
}}
|
||||
title={title}
|
||||
/>
|
||||
</>
|
||||
), []);
|
||||
|
||||
const ActionButton = ({ onClick, children, className }) => (
|
||||
// <div className={`rsssl-action-buttons__inner`}>
|
||||
<button
|
||||
className={`button ${className} rsssl-action-buttons__button`}
|
||||
onClick={onClick}
|
||||
disabled={processing}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
// </div>
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
setModalOpen(false);
|
||||
}
|
||||
|
||||
const handleOpen = () => {
|
||||
setModalOpen(true);
|
||||
}
|
||||
|
||||
const generateActionButtons = useCallback((id) => {
|
||||
return (<div className="rsssl-action-buttons">
|
||||
<ActionButton
|
||||
onClick={() => allowById(id)} className="button-red">
|
||||
{__("Reset", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</div>)
|
||||
}, [moduleName, allowById, blockRegionByCode, allowRegionByCode, blockCountryByCode, allowCountryByCode]);
|
||||
|
||||
|
||||
|
||||
for (const key in data) {
|
||||
const dataItem = {...data[key]};
|
||||
dataItem.action = generateActionButtons(dataItem.id);
|
||||
dataItem.flag = generateFlag(dataItem.iso2_code, dataItem.country_name);
|
||||
dataItem.status = __(dataItem.status = dataItem.status.charAt(0).toUpperCase() + dataItem.status.slice(1), 'really-simple-ssl');
|
||||
data[key] = dataItem;
|
||||
}
|
||||
|
||||
let paginationSet;
|
||||
paginationSet = typeof pagination !== 'undefined';
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(data).length === 0 ) {
|
||||
setTableHeight(100); // Adjust depending on your UI measurements
|
||||
} else {
|
||||
setTableHeight(rowHeight * (paginationSet ? pagination.perPage + 2 : 12)); // Adjust depending on your UI measurements
|
||||
}
|
||||
|
||||
}, [paginationSet, pagination?.perPage, data]);
|
||||
|
||||
useEffect(() => {
|
||||
const filteredData = Object.entries(data)
|
||||
.filter(([_, dataItem]) => {
|
||||
return Object.values(dataItem).some(val => ((val ?? '').toString().toLowerCase().includes(searchTerm.toLowerCase())));
|
||||
})
|
||||
.map(([key, dataItem]) => {
|
||||
const newItem = { ...dataItem };
|
||||
newItem.action = generateActionButtons(newItem.id);
|
||||
newItem.flag = generateFlag(newItem.iso2_code, newItem.country_name);
|
||||
newItem.status = __(newItem.status = newItem.status.charAt(0).toUpperCase() + newItem.status.slice(1), 'really-simple-ssl');
|
||||
return [key, newItem];
|
||||
})
|
||||
.reduce((obj, [key, val]) => ({ ...obj, [key]: val }), {});
|
||||
}, [searchTerm, data]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<TrustIpAddressModal
|
||||
isOpen={modalOpen}
|
||||
onRequestClose={handleClose}
|
||||
value={ipAddress}
|
||||
status={'trusted'}
|
||||
>
|
||||
</TrustIpAddressModal>
|
||||
<div className="rsssl-container">
|
||||
{/*display the add button on left side*/}
|
||||
<AddButton
|
||||
moduleName={moduleName}
|
||||
handleOpen={handleOpen}
|
||||
processing={processing}
|
||||
blockedText={__("Block IP Address", "really-simple-ssl")}
|
||||
allowedText={__("Trust IP Address", "really-simple-ssl")}
|
||||
/>
|
||||
<div className="rsssl-search-bar">
|
||||
<div className="rsssl-search-bar__inner">
|
||||
<div className="rsssl-search-bar__icon"></div>
|
||||
<input
|
||||
type="text"
|
||||
className="rsssl-search-bar__input"
|
||||
placeholder={__("Search", "really-simple-ssl")}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{rowsSelected.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
}}
|
||||
>
|
||||
<div className={"rsssl-multiselect-datatable-form rsssl-primary"}>
|
||||
<div>
|
||||
{__("You have selected %s rows", "really-simple-ssl").replace('%s', rowsSelected.length)}
|
||||
</div>
|
||||
<div className="rsssl-action-buttons">
|
||||
<>
|
||||
<ActionButton
|
||||
onClick={() => allowById(rowsSelected)} className="button-red">
|
||||
{__("Reset", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{DataTable &&
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={Object.values(data).filter((row) => {
|
||||
return Object.values(row).some((val) => ((val ?? '').toString().toLowerCase()).includes(searchTerm.toLowerCase()));
|
||||
})}
|
||||
dense
|
||||
pagination={true}
|
||||
paginationComponentOptions={{
|
||||
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
|
||||
rangeSeparatorText: __('of', 'really-simple-ssl'),
|
||||
noRowsPerPage: false,
|
||||
selectAllRowsItem: false,
|
||||
selectAllRowsItemText: __('All', 'really-simple-ssl'),
|
||||
|
||||
}}
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
selectableRows={true}
|
||||
clearSelectedRows={rowCleared}
|
||||
paginationPerPage={rowsPerPage}
|
||||
onChangePage={handlePageChange}
|
||||
onChangeRowsPerPage={handlePerRowsChange}
|
||||
onSelectedRowsChange={handleSelection}
|
||||
theme="really-simple-plugins"
|
||||
customStyles={customStyles}
|
||||
/> }
|
||||
{!enabled && (
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay"><span
|
||||
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Here you can add IP addresses that should never be blocked by region restrictions.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default WhiteListDatatable;
|
||||
@@ -0,0 +1,202 @@
|
||||
/* Creates A Store For Risk Data using Zustand */
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
|
||||
const WhiteListTableStore = create((set, get) => ({
|
||||
|
||||
processing: false,
|
||||
processing_block: false,
|
||||
dataLoaded: false,
|
||||
dataLoaded_block: false,
|
||||
pagination: {},
|
||||
dataActions: {},
|
||||
WhiteListTable: [],
|
||||
BlockListData: [],
|
||||
rowCleared: false,
|
||||
maskError: false,
|
||||
ipAddress: '',
|
||||
note: '',
|
||||
|
||||
|
||||
fetchWhiteListData: async (action) => {
|
||||
//we check if the processing is already true, if so we return
|
||||
set({processing: true});
|
||||
set({dataLoaded: false});
|
||||
set({rowCleared: true});
|
||||
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action
|
||||
);
|
||||
//now we set the EventLog
|
||||
if (response && response.request_success) {
|
||||
set({WhiteListTable: response, dataLoaded: true, processing: false, pagination: response.pagination});
|
||||
}
|
||||
set({ rowCleared: true });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
set({processing: false});
|
||||
set({rowCleared: false});
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
fetchData: async (action, filter) => {
|
||||
//we check if the processing is already true, if so we return
|
||||
set({processing_block: true});
|
||||
set({rowCleared: true});
|
||||
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action,
|
||||
{
|
||||
filterValue: filter
|
||||
}
|
||||
);
|
||||
//now we set the EventLog
|
||||
if (response && response.request_success) {
|
||||
set({BlockListData: response, dataLoaded: true, processing: false, pagination: response.pagination});
|
||||
}
|
||||
set({ rowCleared: true });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
set({dataLoaded_block: true})
|
||||
set({processing_block: false});
|
||||
set({rowCleared: false});
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
resetRow: async (id, dataActions) => {
|
||||
set({processing: true});
|
||||
let data = {
|
||||
id: id
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_reset_ip', data);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
// Potentially notify the user of success, if needed.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Handle any unsuccessful response if needed.
|
||||
return { success: false, message: response?.message || 'Failed to reset Ip', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Notify the user of an error.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
}
|
||||
,
|
||||
updateRow: async (ip, note, status, filter) => {
|
||||
set({processing: true});
|
||||
let data = {
|
||||
ip_address: ip,
|
||||
note: note,
|
||||
status: status
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_add_white_list_ip', data);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
await get().fetchWhiteListData('rsssl_geo_white_list');
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Handle any unsuccessful response if needed.
|
||||
return { success: false, message: response?.message || 'Failed to add Ip', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Notify the user of an error.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
|
||||
removeRow: async (country, dataActions) => {
|
||||
set({processing: true});
|
||||
let data = {
|
||||
country_code: country
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_remove_blocked_country', data);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
await get().fetchCountryData('rsssl_geo_white_list');
|
||||
await get().fetchData('rsssl_geo_block_list', {filterValue: 'all'});
|
||||
// Potentially notify the user of success, if needed.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Handle any unsuccessful response if needed.
|
||||
return { success: false, message: response?.message || 'Failed to remove country', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Notify the user of an error.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* This function sets the ip address and is used by Cidr and IpAddressInput
|
||||
*/
|
||||
setIpAddress: (ipAddress) => {
|
||||
if(ipAddress.length === 0) {
|
||||
return;
|
||||
}
|
||||
let ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$|^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,4}|((25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9]))|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9]))$/;
|
||||
if (ipAddress.includes('/')) {
|
||||
let finalIp = '';
|
||||
// Split the input into IP and CIDR mask
|
||||
let [ip, mask] = ipAddress.split('/');
|
||||
//if , we change it to .
|
||||
ip = ip.replace(/,/g, '.');
|
||||
if (mask.length <= 0 ) {
|
||||
if (!ipRegex.test(ip)) {
|
||||
set({maskError: true});
|
||||
} else {
|
||||
set({maskError: false});
|
||||
}
|
||||
finalIp = `${ip}/${mask}`;
|
||||
} else {
|
||||
finalIp = mask ? `${ip}/${mask}` : ip;
|
||||
}
|
||||
set({ ipAddress: finalIp })
|
||||
} else {
|
||||
if (!ipRegex.test(ipAddress)) {
|
||||
set({maskError: true});
|
||||
} else {
|
||||
set({maskError: false});
|
||||
}
|
||||
set({ ipAddress: ipAddress.replace(/,/g, '.') })
|
||||
}
|
||||
},
|
||||
|
||||
setNote: (note) => {
|
||||
set({note});
|
||||
},
|
||||
|
||||
resetRange: () => {
|
||||
set({inputRangeValidated: false});
|
||||
set({highestIP: ''});
|
||||
set({lowestIP: ''});
|
||||
set({ipAddress: ''});
|
||||
set({maskError: false});
|
||||
},
|
||||
|
||||
setDataLoaded: (dataLoaded) => {
|
||||
set({dataLoaded});
|
||||
set({dataLoaded_block: dataLoaded});
|
||||
}
|
||||
|
||||
}));
|
||||
|
||||
export default WhiteListTableStore;
|
||||
@@ -0,0 +1,34 @@
|
||||
import Icon from "../utils/Icon";
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import DOMPurify from "dompurify";
|
||||
/**
|
||||
* Render a help notice in the sidebar
|
||||
*/
|
||||
const Help = (props) => {
|
||||
let notice = {...props.help};
|
||||
if ( !notice.title ){
|
||||
notice.title = notice.text;
|
||||
notice.text = false;
|
||||
}
|
||||
let openStatus = props.noticesExpanded ? 'open' : '';
|
||||
//we can use notice.linked_field to create a visual link to the field.
|
||||
|
||||
let target = notice.url && notice.url.indexOf("really-simple-ssl.com") !==-1 ? "_blank" : '_self';
|
||||
return (
|
||||
<div>
|
||||
{ notice.title && notice.text &&
|
||||
<details className={"rsssl-wizard-help-notice rsssl-" + notice.label.toLowerCase()} open={openStatus}>
|
||||
<summary>{notice.title} <Icon name='chevron-down' /></summary>
|
||||
{/*some notices contain html, like for the htaccess notices. A title is required for those options, otherwise the text becomes the title. */}
|
||||
<div dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(notice.text)}}></div>
|
||||
{notice.url && <div className="rsssl-help-more-info"><a target={target} href={notice.url}>{__("More info", "really-simple-ssl")}</a></div>}
|
||||
</details>
|
||||
}
|
||||
{ notice.title && !notice.text &&
|
||||
<div className={"rsssl-wizard-help-notice rsssl-" + notice.label.toLowerCase()}><p>{notice.title}</p></div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Help
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useState, useEffect, memo } from "@wordpress/element";
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import useFields from "../FieldsData";
|
||||
import AutoCompleteControl from "../AutoComplete/AutoCompleteControl";
|
||||
import useHostData from "./HostData";
|
||||
import { __ } from "@wordpress/i18n";
|
||||
import autoCompleteSharedTheme from "../../utils/autoCompleteTheme";
|
||||
const Host = ({ field, showDisabledWhenSaving = true }) => {
|
||||
const { updateField, setChangedField, saveFields, handleNextButtonDisabled } = useFields();
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const { fetchHosts, hosts, hostsLoaded } = useHostData();
|
||||
|
||||
useEffect(() => {
|
||||
if (!hostsLoaded) {
|
||||
fetchHosts();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
handleNextButtonDisabled(disabled);
|
||||
}, [disabled]);
|
||||
|
||||
const onChangeHandler = async (fieldValue) => {
|
||||
if (showDisabledWhenSaving) {
|
||||
setDisabled(true);
|
||||
}
|
||||
updateField(field.id, fieldValue);
|
||||
setChangedField(field.id, fieldValue);
|
||||
await saveFields(true, false);
|
||||
setDisabled(false);
|
||||
};
|
||||
|
||||
let loadedHosts = hostsLoaded ? hosts : [];
|
||||
let options = [];
|
||||
let item = {
|
||||
label: __('Optional - Select your hosting provider.', 'really-simple-ssl'),
|
||||
value: '',
|
||||
};
|
||||
if (field.value.length === 0) {
|
||||
options.push(item);
|
||||
}
|
||||
for (let key in loadedHosts) {
|
||||
if (loadedHosts.hasOwnProperty(key)) {
|
||||
let item = {};
|
||||
item.label = loadedHosts[key].name;
|
||||
item.value = key;
|
||||
options.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={autoCompleteSharedTheme}>
|
||||
<AutoCompleteControl
|
||||
className="rsssl-select"
|
||||
field={field}
|
||||
label={field.label}
|
||||
onChange={(fieldValue) => onChangeHandler(fieldValue)}
|
||||
value={field.value}
|
||||
options={options}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Host);
|
||||
@@ -0,0 +1,25 @@
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
const useHostData = create(( set, get ) => ({
|
||||
hosts: [],
|
||||
hostsLoaded:false,
|
||||
fetchHosts: async ( id ) => {
|
||||
try {
|
||||
const response = await rsssl_api.doAction('get_hosts', { id: id });
|
||||
|
||||
// Handle the response
|
||||
if ( !response ) {
|
||||
console.error('No response received from the server.');
|
||||
return;
|
||||
}
|
||||
let hosts = response.hosts;
|
||||
// Set the roles state with formatted data
|
||||
set({hosts: hosts,hostsLoaded:true });
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
export default useHostData;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import useLearningMode from "./LearningModeData";
|
||||
const ChangeStatus = (props) => {
|
||||
const {updateStatus} = useLearningMode();
|
||||
|
||||
let statusClass = props.item.status==1 ? 'button button-primary rsssl-status-allowed' : 'button button-default rsssl-status-revoked';
|
||||
let label = props.item.status==1 ? __("Revoke", "really-simple-ssl") : __("Allow", "really-simple-ssl");
|
||||
return (
|
||||
<button onClick={ () => updateStatus( props.item.status, props.item, props.field.id ) } className={statusClass}>{label}</button>
|
||||
)
|
||||
}
|
||||
export default ChangeStatus
|
||||
@@ -0,0 +1,16 @@
|
||||
import useLearningMode from "./LearningModeData";
|
||||
import {__} from "@wordpress/i18n";
|
||||
|
||||
const Delete = (props) => {
|
||||
const {deleteData} = useLearningMode();
|
||||
|
||||
return (
|
||||
<button type="button" className="button button-red rsssl-learning-mode-delete" onClick={ () => deleteData( props.item, props.field.id ) }>
|
||||
{/*<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" height="16" >*/}
|
||||
{/* <path fill="#000000" d="M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z"/>*/}
|
||||
{/*</svg>*/}
|
||||
{__("Delete", "really-simple-ssl")}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
export default Delete
|
||||
@@ -0,0 +1,400 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {useState,useEffect} from '@wordpress/element';
|
||||
import ChangeStatus from "./ChangeStatus";
|
||||
import Delete from "./Delete";
|
||||
import Icon from "../../utils/Icon";
|
||||
import useFields from "./../FieldsData";
|
||||
import UseLearningMode from "./LearningModeData";
|
||||
import {Button} from "@wordpress/components";
|
||||
import React from "react";
|
||||
import ManualCspAdditionModal from "./ManualCspAdditionModal";
|
||||
import AddButton from "../GeoBlockList/AddButton";
|
||||
import ManualCspAddition from "./ManualCspAddition";
|
||||
|
||||
const LearningMode = (props) => {
|
||||
|
||||
const {updateField, getFieldValue, getField, setChangedField, highLightField, saveFields} = useFields();
|
||||
const {fetchLearningModeData, learningModeData, dataLoaded, updateStatus, deleteData} = UseLearningMode();
|
||||
const {manualAdditionProcessing} = ManualCspAddition();
|
||||
|
||||
//used to show if a feature is already enforced by a third party
|
||||
const [enforcedByThirdparty, setEnforcedByThirdparty] = useState(0);
|
||||
//toggle from enforced to not enforced
|
||||
const [enforce, setEnforce] = useState(0);
|
||||
//toggle from learning mode to not learning mode
|
||||
const [learningMode, setLearningMode] = useState(0);
|
||||
//set learning mode to completed
|
||||
const [learningModeCompleted, setLearningModeCompleted] = useState(0);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
//check if learningmode has been enabled at least once
|
||||
const [lmEnabledOnce, setLmEnabledOnce] = useState(0);
|
||||
//filter the data
|
||||
const [filterValue, setFilterValue] = useState(-1);
|
||||
//the value that is used to enable or disable this feature. On or of.
|
||||
const [controlField, setControlField] = useState(false);
|
||||
// the value that is used to select and deselect rows
|
||||
const [rowsSelected, setRowsSelected] = useState([]);
|
||||
const [rowCleared, setRowCleared] = useState(false);
|
||||
|
||||
const [DataTable, setDataTable] = useState(null);
|
||||
const [theme, setTheme] = useState(null);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
useEffect( () => {
|
||||
import('react-data-table-component').then(({ default: DataTable, createTheme }) => {
|
||||
setDataTable(() => DataTable);
|
||||
setTheme(() => createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light'));
|
||||
});
|
||||
|
||||
}, []);
|
||||
|
||||
|
||||
/**
|
||||
* Styling
|
||||
*/
|
||||
const conditionalRowStyles = [
|
||||
{
|
||||
when: row => row.status ==0,
|
||||
classNames: ['rsssl-datatables-revoked'],
|
||||
},
|
||||
];
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for head cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for data cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
;
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*/
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
await fetchLearningModeData(props.field.id);
|
||||
let controlField = getField(props.field.control_field );
|
||||
let enforced_by_thirdparty = controlField.value === 'enforced-by-thirdparty';
|
||||
let enforce = enforced_by_thirdparty || controlField.value === 'enforce';
|
||||
|
||||
setControlField(controlField);
|
||||
setEnforcedByThirdparty(enforced_by_thirdparty);
|
||||
setLearningModeCompleted(controlField.value==='completed');
|
||||
setHasError(controlField.value==='error');
|
||||
setLmEnabledOnce(getFieldValue(props.field.control_field+'_lm_enabled_once'))
|
||||
setEnforce(enforce);
|
||||
setLearningMode(controlField.value === 'learning_mode');
|
||||
}
|
||||
run();
|
||||
}, [enforce, learningMode] );
|
||||
|
||||
const toggleEnforce = async (e, enforceValue) => {
|
||||
e.preventDefault();
|
||||
//enforce this setting
|
||||
let controlFieldValue = enforceValue==1 ? 'enforce' : 'disabled';
|
||||
setEnforce(enforceValue);
|
||||
setLearningModeCompleted(0);
|
||||
setLearningMode(0);
|
||||
setChangedField(controlField.id, controlFieldValue);
|
||||
updateField(controlField.id, controlFieldValue);
|
||||
await saveFields(true, false);
|
||||
//await fetchLearningModeData();
|
||||
}
|
||||
|
||||
|
||||
const toggleLearningMode = async (e) => {
|
||||
e.preventDefault();
|
||||
let lmEnabledOnceField = getField(props.field.control_field+'_lm_enabled_once');
|
||||
if ( learningMode ) {
|
||||
setLmEnabledOnce(1);
|
||||
updateField(lmEnabledOnceField.id, 1);
|
||||
}
|
||||
|
||||
let controlFieldValue;
|
||||
if ( learningMode || learningModeCompleted ) {
|
||||
setLearningMode(0);
|
||||
controlFieldValue = 'disabled';
|
||||
} else {
|
||||
setLearningMode(1);
|
||||
controlFieldValue = 'learning_mode';
|
||||
}
|
||||
setLearningModeCompleted(0);
|
||||
setChangedField(controlField.id, controlFieldValue);
|
||||
updateField(controlField.id, controlFieldValue);
|
||||
setChangedField(lmEnabledOnceField.id, lmEnabledOnceField.value);
|
||||
updateField(lmEnabledOnceField, lmEnabledOnceField.value);
|
||||
await saveFields(true, false);
|
||||
}
|
||||
|
||||
const Filter = () => (
|
||||
<>
|
||||
<select onChange={ ( e ) => setFilterValue(e.target.value) } value={filterValue}>
|
||||
<option value="-1" >{__("All", "really-simple-ssl")}</option>
|
||||
<option value="1" >{__("Allowed", "really-simple-ssl")}</option>
|
||||
<option value="0" >{__("Blocked", "really-simple-ssl")}</option>
|
||||
</select>
|
||||
</>
|
||||
);
|
||||
|
||||
let field = props.field;
|
||||
let configuringString = __(" The %s is now in report-only mode and will collect directives. This might take a while. Afterwards you can Exit, Edit and Enforce these Directives.", "really-simple-ssl").replace('%s', field.label);
|
||||
let disabledString = __("%s has been disabled.", "really-simple-ssl").replace('%s', field.label);
|
||||
let enforcedString = __("%s is enforced.", "really-simple-ssl").replace('%s', field.label);
|
||||
let enforceDisabled = !lmEnabledOnce;
|
||||
if (enforcedByThirdparty) disabledString = __("%s is already set outside Really Simple Security.", "really-simple-ssl").replace('%s', field.label);
|
||||
let highLightClass = 'rsssl-field-wrap';
|
||||
if ( highLightField===props.field.id ) {
|
||||
highLightClass = 'rsssl-field-wrap rsssl-highlight';
|
||||
}
|
||||
//build our header
|
||||
let columns = [];
|
||||
field.columns.forEach(function(item, i) {
|
||||
let newItem = {
|
||||
name: item.name,
|
||||
sortable: item.sortable,
|
||||
width: item.width,
|
||||
selector: item.column === 'documenturi' || item.column === 'method'
|
||||
? row => <span title={row[item.column]}>{row[item.column]}</span>: row => row[item.column],
|
||||
}
|
||||
columns.push(newItem);
|
||||
});
|
||||
|
||||
let data = learningModeData;
|
||||
data = data.filter(item => item.status<2);
|
||||
if (filterValue!=-1) {
|
||||
data = data.filter(item => item.status==filterValue);
|
||||
}
|
||||
for (const item of data){
|
||||
if (item.login_status) item.login_statusControl = item.login_status == 1 ? __("success", "really-simple-ssl") : __("failed", "really-simple-ssl");
|
||||
item.statusControl = <ChangeStatus item={item} field={props.field} />;
|
||||
item.deleteControl = <Delete item={item} field={props.field}/>;
|
||||
item.grouped = <div className="rsssl-action-buttons">
|
||||
<ChangeStatus item={item} field={props.field} />
|
||||
<Delete item={item} field={props.field}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
const handleMultiRowStatus = (status, selectedRows, type) => {
|
||||
selectedRows.forEach(row => {
|
||||
//the updateItemId allows us to update one specific item in a field set.
|
||||
updateStatus(status, row, type);
|
||||
});
|
||||
setRowCleared(true);
|
||||
setRowsSelected([]);
|
||||
// Reset rowCleared back to false after the DataTable has re-rendered
|
||||
setTimeout(() => setRowCleared(false), 0);
|
||||
}
|
||||
|
||||
const handleMultiRowDelete = ( selectedRows, type) => {
|
||||
selectedRows.forEach(row => {
|
||||
//the updateItemId allows us to update one specific item in a field set.
|
||||
deleteData( row, type );
|
||||
});
|
||||
setRowCleared(true);
|
||||
setRowsSelected([]);
|
||||
// Reset rowCleared back to false after the DataTable has re-rendered
|
||||
setTimeout(() => setRowCleared(false), 0);
|
||||
}
|
||||
function handleSelection(state) {
|
||||
setRowCleared(false);
|
||||
setRowsSelected(state.selectedRows);
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setModalOpen(false);
|
||||
}
|
||||
|
||||
const handleOpen = () => {
|
||||
setModalOpen(true);
|
||||
}
|
||||
|
||||
if (!DataTable || !theme) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.field.id === 'content_security_policy_source_directives' && (<>
|
||||
<ManualCspAdditionModal
|
||||
isOpen={modalOpen}
|
||||
onRequestClose={handleClose}
|
||||
status={'blocked'}
|
||||
directives={props.field.modal.options}
|
||||
parentId={props.field.id}
|
||||
/>
|
||||
<div className="rsssl-container" style={{paddingTop: "0px"}}>
|
||||
<AddButton
|
||||
handleOpen={handleOpen}
|
||||
processing={manualAdditionProcessing}
|
||||
allowedText={__("Add Entry", "really-simple-ssl")}
|
||||
/>
|
||||
</div>
|
||||
</>)}
|
||||
{!dataLoaded && <>
|
||||
<div className="rsssl-learningmode-placeholder">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</>}
|
||||
{rowsSelected.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={"rsssl-multiselect-datatable-form rsssl-primary"}
|
||||
>
|
||||
<div>
|
||||
{__("You have selected", "really-simple-ssl")} {rowsSelected.length} {__("rows", "really-simple-ssl")}
|
||||
</div>
|
||||
|
||||
<div className="rsssl-action-buttons">
|
||||
{(Number(filterValue) === -1 || Number(filterValue) === 0) &&
|
||||
<div className="rsssl-action-buttons__inner">
|
||||
<Button
|
||||
// className={"button button-red rsssl-action-buttons__button"}
|
||||
className={"button button-secondary rsssl-status-allowed rsssl-action-buttons__button"}
|
||||
onClick={() => handleMultiRowStatus(0, rowsSelected, props.field.id)}
|
||||
>
|
||||
{__('Allow', 'really-simple-ssl')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
{(Number(filterValue) === -1 || Number(filterValue) === 1) &&
|
||||
<div className="rsssl-action-buttons__inner">
|
||||
<Button
|
||||
// className={"button button-red rsssl-action-buttons__button"}
|
||||
className={"button button-primary rsssl-action-buttons__button"}
|
||||
onClick={() => handleMultiRowStatus(1, rowsSelected, props.field.id)}
|
||||
>
|
||||
{__('Revoke', 'really-simple-ssl')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
<div className="rsssl-action-buttons__inner">
|
||||
<Button
|
||||
// className={"button button-red rsssl-action-buttons__button"}
|
||||
className={"button button-red rsssl-action-buttons__button"}
|
||||
onClick={() => handleMultiRowDelete(rowsSelected, props.field.id)}
|
||||
>
|
||||
{__('Remove', 'really-simple-ssl')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{dataLoaded && <>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
dense
|
||||
pagination
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
theme={theme}
|
||||
customStyles={customStyles}
|
||||
conditionalRowStyles={conditionalRowStyles}
|
||||
paginationComponentOptions={{
|
||||
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
|
||||
rangeSeparatorText: __('of', 'really-simple-ssl'),
|
||||
noRowsPerPage: false,
|
||||
selectAllRowsItem: false,
|
||||
selectAllRowsItemText: __('All', 'really-simple-ssl'),
|
||||
}}
|
||||
selectableRows
|
||||
selectableRowsHighlight={true}
|
||||
onSelectedRowsChange={handleSelection}
|
||||
clearSelectedRows={rowCleared}
|
||||
/></>
|
||||
}
|
||||
<div className={"rsssl-learning-mode-footer"} style={{marginLeft:'0px'}}>
|
||||
{hasError && <div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay">
|
||||
<span
|
||||
className="rsssl-progress-status rsssl-learning-mode-error">{__("Error detected", "really-simple-ssl")}</span>
|
||||
{__("%s cannot be implemented due to server limitations. Check your notices for the detected issue.", "really-simple-ssl").replace('%s', field.label)}
|
||||
<a className="rsssl-learning-mode-link" href="#"
|
||||
onClick={(e) => toggleEnforce(e, false)}>{__("Disable", "really-simple-ssl")}</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{!hasError && <>
|
||||
{enforce != 1 && <button disabled={enforceDisabled} className="button button-primary"
|
||||
onClick={(e) => toggleEnforce(e, true)}>{__("Enforce", "really-simple-ssl")}</button>}
|
||||
{!enforcedByThirdparty && enforce == 1 && <button className="button"
|
||||
onClick={(e) => toggleEnforce(e, false)}>{__("Disable", "really-simple-ssl")}</button>}
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
disabled={enforce}
|
||||
checked={learningMode == 1}
|
||||
value={learningMode}
|
||||
onChange={(e) => toggleLearningMode(e)}
|
||||
/>
|
||||
{__("Enable Learning Mode to configure automatically", "really-simple-ssl")}
|
||||
</label>
|
||||
{enforce == 1 && <div className="rsssl-locked">
|
||||
<div className="rsssl-shield-overlay">
|
||||
<Icon name="shield" size="80px"/>
|
||||
</div>
|
||||
<div className="rsssl-locked-overlay">
|
||||
<span
|
||||
className="rsssl-progress-status rsssl-learning-mode-enforced">{__("Enforced", "really-simple-ssl")}</span>
|
||||
{enforcedString}
|
||||
<a className="rsssl-learning-mode-link" href="#"
|
||||
onClick={(e) => toggleEnforce(e)}>{__("Disable to configure", "really-simple-ssl")}</a>
|
||||
</div>
|
||||
</div>}
|
||||
{learningMode == 1 && <div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay">
|
||||
<span
|
||||
className="rsssl-progress-status rsssl-learning-mode">{__("Learning Mode", "really-simple-ssl")}</span>
|
||||
{configuringString}
|
||||
<a className="rsssl-learning-mode-link" href="#"
|
||||
onClick={(e) => toggleLearningMode(e)}>{__("Exit", "really-simple-ssl")}</a>
|
||||
</div>
|
||||
</div>}
|
||||
{learningModeCompleted == 1 && <div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay">
|
||||
<span
|
||||
className="rsssl-progress-status rsssl-learning-mode-completed">{__("Learning Mode", "really-simple-ssl")}</span>
|
||||
{__("We finished the configuration.", "really-simple-ssl")}
|
||||
<a className="rsssl-learning-mode-link" href="#"
|
||||
onClick={(e) => toggleLearningMode(e)}>{__("Review the settings and enforce the policy", "really-simple-ssl")}</a>
|
||||
</div>
|
||||
</div>}
|
||||
{rsssl_settings.pro_plugin_active && props.disabled && <div className="rsssl-locked ">
|
||||
<div className="rsssl-locked-overlay">
|
||||
{!enforcedByThirdparty && <span
|
||||
className="rsssl-progress-status rsssl-disabled">{__("Disabled", "really-simple-ssl")}</span>}
|
||||
{enforcedByThirdparty && <span
|
||||
className="rsssl-progress-status rsssl-learning-mode-enforced">{__("Enforced", "really-simple-ssl")}</span>}
|
||||
{disabledString}
|
||||
</div>
|
||||
</div>}
|
||||
</>
|
||||
}
|
||||
<Filter/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LearningMode
|
||||
@@ -0,0 +1,93 @@
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
|
||||
const UseLearningMode = create(( set, get ) => ({
|
||||
learningModeData: [],
|
||||
dataLoaded: false,
|
||||
fetchLearningModeData: async (type) => {
|
||||
let data = {};
|
||||
data.type = type;
|
||||
data.lm_action = 'get';
|
||||
let learningModeData = await rsssl_api.doAction('learning_mode_data', data).then((response) => {
|
||||
return response;
|
||||
})
|
||||
|
||||
if ( typeof learningModeData === 'object' && learningModeData.request_success === true ) {
|
||||
learningModeData = Object.values(learningModeData);
|
||||
}
|
||||
|
||||
if ( !Array.isArray(learningModeData) ) {
|
||||
learningModeData = [];
|
||||
}
|
||||
set({
|
||||
learningModeData: learningModeData,
|
||||
dataLoaded:true,
|
||||
});
|
||||
},
|
||||
updateStatus: async (enabled, updateItem, type) => {
|
||||
let learningModeData = get().learningModeData;
|
||||
let data = {};
|
||||
data.type = type;
|
||||
data.updateItemId = updateItem.id;
|
||||
data.enabled = enabled==1 ? 0 : 1;
|
||||
data.lm_action = 'update';
|
||||
|
||||
//for fast UX feel, update the state before we post
|
||||
for (const item of learningModeData){
|
||||
if (updateItem.id === item.id && item.status) {
|
||||
item.status = data.enabled;
|
||||
}
|
||||
}
|
||||
set({
|
||||
learningModeData: learningModeData,
|
||||
});
|
||||
learningModeData = await rsssl_api.doAction('learning_mode_data', data).then((response) => {
|
||||
return response;
|
||||
})
|
||||
if ( typeof learningModeData === 'object' ) {
|
||||
learningModeData = Object.values(learningModeData);
|
||||
}
|
||||
if ( !Array.isArray(learningModeData) ) {
|
||||
learningModeData = [];
|
||||
}
|
||||
set({
|
||||
learningModeData: learningModeData,
|
||||
dataLoaded:true,
|
||||
});
|
||||
},
|
||||
deleteData: async (deleteItem, type) => {
|
||||
let learningModeData = get().learningModeData;
|
||||
|
||||
let data = {};
|
||||
data.type = type;
|
||||
data.updateItemId = deleteItem.id;
|
||||
data.lm_action = 'delete';
|
||||
//for fast UX feel, update the state before we post
|
||||
learningModeData.forEach(function(item, i) {
|
||||
if (item.id === deleteItem.id) {
|
||||
learningModeData.splice(i, 1);
|
||||
}
|
||||
});
|
||||
set({
|
||||
learningModeData: learningModeData,
|
||||
});
|
||||
learningModeData = await rsssl_api.doAction('learning_mode_data', data).then((response) => {
|
||||
return response;
|
||||
})
|
||||
if ( typeof learningModeData === 'object' ) {
|
||||
learningModeData = Object.values(learningModeData);
|
||||
}
|
||||
if ( !Array.isArray(learningModeData) ) {
|
||||
learningModeData = [];
|
||||
}
|
||||
set({
|
||||
learningModeData: learningModeData,
|
||||
dataLoaded:true,
|
||||
});
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
export default UseLearningMode;
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { create } from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
|
||||
const ManualCspAddition = create((set, get) => ({
|
||||
manualAdditionProcessing: false,
|
||||
manualAdditionData: [],
|
||||
manualAdditionDataLoaded: false,
|
||||
cspUri: '',
|
||||
directive: '',
|
||||
|
||||
setDirective: (directive) => set({ directive }),
|
||||
setCspUri: (cspUri) => set({ cspUri }),
|
||||
setDataLoaded: (manualAdditionDataLoaded) => set({ manualAdditionDataLoaded }),
|
||||
addManualCspEntry: async (cspUri, directive) => {
|
||||
let response;
|
||||
|
||||
set({ manualAdditionProcessing: true });
|
||||
|
||||
try {
|
||||
response = await rsssl_api.doAction('rsssl_csp_uri_add', { cspUri, directive });
|
||||
if (response.request_success) {
|
||||
set({ manualAdditionDataLoaded: false });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
set({ manualAdditionProcessing: false, manualAdditionDataLoaded: true });
|
||||
}
|
||||
|
||||
// Should contain keys "success" and "message";
|
||||
return response;
|
||||
}
|
||||
}));
|
||||
|
||||
export default ManualCspAddition;
|
||||
@@ -0,0 +1,205 @@
|
||||
import {useEffect, useMemo, useRef} from '@wordpress/element';
|
||||
import {Modal, Button} from "@wordpress/components";
|
||||
import SelectControl from "../SelectControl.js"
|
||||
import {__} from "@wordpress/i18n";
|
||||
import FieldsData from "../FieldsData";
|
||||
import ManualCspAddition from "./ManualCspAddition";
|
||||
import UseLearningMode from "./LearningModeData";
|
||||
|
||||
const ManualCspAdditionModal = (props) => {
|
||||
|
||||
const cspUriRef = useRef(null);
|
||||
const {manualAdditionProcessing, directive, setCspUri, setDirective, addManualCspEntry} = ManualCspAddition();
|
||||
const {fetchLearningModeData} = UseLearningMode();
|
||||
const {showSavedSettingsNotice} = FieldsData();
|
||||
const directiveOptions = useMemo(() => getModalSelectOptions(), []);
|
||||
|
||||
async function submitManualCspAddition() {
|
||||
if (!cspUriRef.current || !cspUriRef.current.value) {
|
||||
return showSavedSettingsNotice(__('Something went wrong while saving the manual CSP entry.'), 'error');
|
||||
}
|
||||
|
||||
if (!cspUriRef.current.value.length || !directive.length) {
|
||||
return showSavedSettingsNotice(__('Please enter both the "URI" and the "Directive" before saving.'), 'error');
|
||||
}
|
||||
|
||||
await addManualCspEntry(cspUriRef.current.value, directive).then((response) => {
|
||||
if (response.success === false) {
|
||||
return showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
|
||||
showSavedSettingsNotice(response.message);
|
||||
|
||||
// Re-render table to show new addition
|
||||
fetchLearningModeData(props.parentId);
|
||||
|
||||
clearManualCspAdditionModalFields();
|
||||
return closeManualCspAdditionModal();
|
||||
});
|
||||
}
|
||||
|
||||
function clearManualCspAdditionModalFields() {
|
||||
setCspUri('');
|
||||
setDirective('');
|
||||
|
||||
if (cspUriRef.current) {
|
||||
cspUriRef.current.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the value of the directives passed via the props to the value and
|
||||
* label of the options in the dropdown. We do this because the label in
|
||||
* RSSSL()->headers->directives contains excessive information
|
||||
*
|
||||
* @returns {{value: *, label: *}[]}
|
||||
*/
|
||||
function getModalSelectOptions()
|
||||
{
|
||||
return Object.keys(props.directives).map(key => ({
|
||||
value: props.directives[key],
|
||||
label: props.directives[key]
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method should be called when a user clicked the cancel button
|
||||
*/
|
||||
function handleCancel() {
|
||||
clearManualCspAdditionModalFields();
|
||||
closeManualCspAdditionModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method can be used to submit the manual CSP entry with the "Enter" key
|
||||
*/
|
||||
function submitWithEnter(event) {
|
||||
if (event.key === 'Enter') {
|
||||
submitManualCspAddition();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When modal is used the closing-callback for the modal can be bind to a
|
||||
* method by using the "onRequestClose" property.
|
||||
* @see settings/src/Settings/LearningMode/LearningMode.js
|
||||
*/
|
||||
function closeManualCspAdditionModal()
|
||||
{
|
||||
props.onRequestClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default value to the first value in props.directives.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!directive) {
|
||||
setDirective(props.directives[0]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Don't render when the modal isn't open.
|
||||
*/
|
||||
if (!props.isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={__("Add Entry", "really-simple-ssl")}
|
||||
shouldCloseOnClickOutside={true}
|
||||
shouldCloseOnEsc={true}
|
||||
overlayClassName="rsssl-modal-overlay"
|
||||
className="rsssl-modal"
|
||||
onRequestClose={closeManualCspAdditionModal}
|
||||
>
|
||||
<div className="modal-content">
|
||||
<div className="modal-body"
|
||||
style={{
|
||||
padding: "0.5em",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "95%",
|
||||
height: "100%",
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
<div style={{position: 'relative'}}>
|
||||
<label
|
||||
htmlFor={'cspUri'}
|
||||
className={'rsssl-label'}
|
||||
>{__('URI', 'really-simple-ssl')}</label>
|
||||
<input
|
||||
id={'cspUri'}
|
||||
type={'text'}
|
||||
name={'cspUri'}
|
||||
ref={cspUriRef}
|
||||
onKeyDown={submitWithEnter}
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
disabled={manualAdditionProcessing}
|
||||
/>
|
||||
</div>
|
||||
<div style={{marginTop: '10px'}}>
|
||||
<SelectControl
|
||||
field={
|
||||
{
|
||||
// workaround for working with SelectControl
|
||||
id: 'directive',
|
||||
}
|
||||
}
|
||||
id={'directive'}
|
||||
label={__('Directive', 'really-simple-ssl')}
|
||||
name={'directive'}
|
||||
value={directive}
|
||||
onChangeHandler={(value) => setDirective(value)}
|
||||
options={directiveOptions}
|
||||
style={{
|
||||
label: {
|
||||
display: 'block',
|
||||
},
|
||||
select: {
|
||||
maxWidth: 'unset',
|
||||
width: '100%',
|
||||
}
|
||||
}}
|
||||
disabled={manualAdditionProcessing}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<div
|
||||
className={'rsssl-grid-item-footer'}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
padding: '1em',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
isSecondary
|
||||
onClick={handleCancel}
|
||||
style={{marginRight: '10px'}}
|
||||
>
|
||||
{__("Cancel", "really-simple-ssl")}
|
||||
</Button>
|
||||
<Button
|
||||
isPrimary
|
||||
onClick={submitManualCspAddition}
|
||||
>
|
||||
{__("Add", "really-simple-ssl")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ManualCspAdditionModal;
|
||||
@@ -0,0 +1,61 @@
|
||||
import TaskElement from "../../Dashboard/TaskElement";
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import useFields from "./../FieldsData";
|
||||
import useLicense from "./LicenseData";
|
||||
import {useEffect} from "@wordpress/element";
|
||||
const License = ({field, isOnboarding}) => {
|
||||
const {fields, setChangedField, updateField} = useFields();
|
||||
const {toggleActivation, licenseStatus, setLicenseStatus, notices, setNotices, setLoadingState} = useLicense();
|
||||
|
||||
useEffect(() => {
|
||||
setLoadingState();
|
||||
}, []);
|
||||
const getLicenseNotices = () => {
|
||||
return rsssl_api.runTest('licenseNotices', 'refresh').then( ( response ) => {
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
useEffect( () => {
|
||||
getLicenseNotices().then(( response ) => {
|
||||
setLicenseStatus(response.licenseStatus);
|
||||
setNotices(response.notices);
|
||||
});
|
||||
}, [fields] );
|
||||
|
||||
const onChangeHandler = (fieldValue) => {
|
||||
setChangedField( field.id, fieldValue )
|
||||
updateField(field.id, fieldValue);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="components-base-control">
|
||||
<div className="components-base-control__field">
|
||||
{ !isOnboarding && <label className="components-base-control__label" htmlFor={field.id}>
|
||||
{field.label}
|
||||
</label> }
|
||||
<div className="rsssl-license-field">
|
||||
<input
|
||||
className="components-text-control__input"
|
||||
type="password"
|
||||
id={field.id}
|
||||
value={field.value}
|
||||
onChange={(e) => onChangeHandler(e.target.value)}
|
||||
/>
|
||||
{ !isOnboarding &&
|
||||
<button className="button button-default" onClick={() => toggleActivation(field.value)}>
|
||||
{licenseStatus === 'valid' && <>{__('Deactivate', 'really-simple-ssl')}</>}
|
||||
{licenseStatus !== 'valid' && <>{__('Activate', 'really-simple-ssl')}</>}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{ notices.map((notice, i) => (
|
||||
<TaskElement key={'task-' + i} index={i} notice={notice} highLightField="" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default License;
|
||||
@@ -0,0 +1,52 @@
|
||||
import {create} from 'zustand';
|
||||
import useFields from "../FieldsData";
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import {__} from "@wordpress/i18n";
|
||||
|
||||
const UseLicenseData = create(( set, get ) => ({
|
||||
|
||||
licenseStatus: rsssl_settings.licenseStatus,
|
||||
setLicenseStatus: (licenseStatus) => set(state => ({ licenseStatus })),
|
||||
notices:[],
|
||||
setNotices: (notices) => set(state => ({ notices })),
|
||||
setLoadingState: () => {
|
||||
const disabledState = {output: {
|
||||
dismissible: false,
|
||||
icon: 'skeleton',
|
||||
label: __( 'Loading', 'really-simple-ssl' ),
|
||||
msg: false,
|
||||
plusone: false,
|
||||
url: false
|
||||
}
|
||||
};
|
||||
const skeletonNotices = [
|
||||
disabledState,
|
||||
disabledState,
|
||||
disabledState
|
||||
];
|
||||
set({notices:skeletonNotices})
|
||||
},
|
||||
toggleActivation: async (licenseKey) => {
|
||||
get().setLoadingState();
|
||||
if ( get().licenseStatus==='valid' ) {
|
||||
await rsssl_api.runTest('deactivate_license').then( ( response ) => {
|
||||
set({
|
||||
notices: response.notices,
|
||||
licenseStatus: response.licenseStatus,
|
||||
})
|
||||
});
|
||||
} else {
|
||||
let data = {};
|
||||
data.license = licenseKey;
|
||||
await rsssl_api.doAction('activate_license', data).then( ( response ) => {
|
||||
set({
|
||||
notices: response.notices,
|
||||
licenseStatus: response.licenseStatus,
|
||||
})
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
export default UseLicenseData;
|
||||
@@ -0,0 +1,126 @@
|
||||
import {useEffect, useState} from '@wordpress/element';
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
} from "@wordpress/components";
|
||||
import IpAddressDataTableStore from "./IpAddressDataTableStore";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import IpAddressInput from "./IpAddressInput";
|
||||
import EventLogDataTableStore from "../EventLog/EventLogDataTableStore";
|
||||
import FieldsData from "../FieldsData";
|
||||
|
||||
const AddIpAddressModal = (props) => {
|
||||
const { inputRangeValidated, fetchCidrData, ipAddress, setIpAddress, maskError, dataLoaded, updateRow, resetRange} = IpAddressDataTableStore();
|
||||
const [rangeDisplay, setRangeDisplay] = useState(false);
|
||||
const {fetchDynamicData} = EventLogDataTableStore();
|
||||
const {showSavedSettingsNotice} = FieldsData();
|
||||
|
||||
useEffect(() => {
|
||||
//we validate the range
|
||||
if (inputRangeValidated) {
|
||||
//we get the mask
|
||||
fetchCidrData('get_mask_from_range')
|
||||
}
|
||||
}, [inputRangeValidated]);
|
||||
|
||||
async function handleSubmit() {
|
||||
let status = props.status;
|
||||
// we check if statusSelected is not empty
|
||||
if (ipAddress && maskError === false) {
|
||||
await updateRow(ipAddress, status, props.dataActions).then((response) => {
|
||||
if (response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
});
|
||||
//we clear the input
|
||||
resetRange();
|
||||
//we close the modal
|
||||
props.onRequestClose();
|
||||
//we fetch the data again
|
||||
fetchDynamicData('event_log')
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
// Reset all local state
|
||||
setRangeDisplay(false);
|
||||
resetRange();
|
||||
|
||||
// Close the modal
|
||||
props.onRequestClose();
|
||||
}
|
||||
if (!props.isOpen) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
title={__("Add IP Address", "really-simple-ssl")}
|
||||
shouldCloseOnClickOutside={true}
|
||||
shouldCloseOnEsc={true}
|
||||
overlayClassName="rsssl-modal-overlay"
|
||||
className="rsssl-modal"
|
||||
onRequestClose={props.onRequestClose}
|
||||
>
|
||||
<div className="modal-content">
|
||||
<div className="modal-body"
|
||||
style={{
|
||||
padding: "1em",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "95%",
|
||||
height: "100%",
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<IpAddressInput
|
||||
label={__("IP Address", "really-simple-ssl")}
|
||||
id="ip-address"
|
||||
name="ip-address"
|
||||
showSwitch={true}
|
||||
value={ipAddress}
|
||||
onChange={(e) => setIpAddress(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
{/*//we add two buttons here for add row and cancel*/}
|
||||
<div
|
||||
className={'rsssl-grid-item-footer'}
|
||||
style
|
||||
={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
padding: '1em',
|
||||
}
|
||||
}
|
||||
>
|
||||
<Button
|
||||
isSecondary
|
||||
onClick={handleCancel}
|
||||
style={{ marginRight: '10px' }}
|
||||
|
||||
>
|
||||
{__("Cancel", "really-simple-ssl")}
|
||||
</Button>
|
||||
<Button
|
||||
isPrimary
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{__("Add", "really-simple-ssl")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddIpAddressModal;
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useState} from '@wordpress/element';
|
||||
import {Modal, Button} from "@wordpress/components";
|
||||
import UserDataTableStore from "./UserDataTableStore";
|
||||
import EventLogDataTableStore from "../EventLog/EventLogDataTableStore";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import FieldsData from "../FieldsData";
|
||||
|
||||
|
||||
const AddUserModal = (props) => {
|
||||
if (!props.isOpen) return null;
|
||||
|
||||
const {updateRow} = UserDataTableStore();
|
||||
const {fetchDynamicData} = EventLogDataTableStore();
|
||||
const [user, setUser] = useState('');
|
||||
const {showSavedSettingsNotice} = FieldsData();
|
||||
|
||||
async function handleSubmit() {
|
||||
let status = props.status;
|
||||
// we check if statusSelected is not empty
|
||||
if (user !== '') {
|
||||
await updateRow(user, status, props.dataActions).then((response) => {
|
||||
if(response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
});
|
||||
//we clear the input
|
||||
setUser('');
|
||||
await fetchDynamicData('event_log');
|
||||
}
|
||||
props.onRequestClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={__("Add User", "really-simple-ssl")}
|
||||
shouldCloseOnClickOutside={true}
|
||||
shouldCloseOnEsc={true}
|
||||
overlayClassName="rsssl-modal-overlay"
|
||||
className="rsssl-modal"
|
||||
onRequestClose={props.onRequestClose}
|
||||
>
|
||||
<div className="modal-content">
|
||||
<div className="modal-body"
|
||||
style={{
|
||||
padding: "1em",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "95%",
|
||||
height: "100%",
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
<label htmlFor="username"
|
||||
className="rsssl-label"
|
||||
>{__("Username", "really-simple-ssl")}</label>
|
||||
<input
|
||||
type="text"
|
||||
className="rsssl-input full"
|
||||
id="username"
|
||||
name="username"
|
||||
onChange={(e) => setUser(e.target.value)}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
{/*//we add two buttons here for add row and cancel*/}
|
||||
<div
|
||||
className={'rsssl-grid-item-footer'}
|
||||
style
|
||||
={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
padding: '1em',
|
||||
}
|
||||
}
|
||||
>
|
||||
<Button
|
||||
isSecondary
|
||||
onClick={props.onRequestClose}
|
||||
style={{marginRight: '10px'}}
|
||||
|
||||
>
|
||||
{__("Cancel", "really-simple-ssl")}
|
||||
</Button>
|
||||
<Button
|
||||
isPrimary
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{__("Add", "really-simple-ssl")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddUserModal;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useState } from "@wordpress/element";
|
||||
import IpAddressDataTableStore from "./IpAddressDataTableStore";
|
||||
import IpAddressInput from "./IpAddressInput";
|
||||
|
||||
const Cidr = () => {
|
||||
const [lowestIP, setLowestIP] = useState("");
|
||||
const [highestIP, setHighestIP] = useState("");
|
||||
const { validateIpRange } = IpAddressDataTableStore();
|
||||
|
||||
const cleanupIpAddress = (ipAddress) => {
|
||||
return ipAddress.replace(/,/g, '.');
|
||||
}
|
||||
|
||||
const handleLowestIPChange = (ip) => {
|
||||
setLowestIP(cleanupIpAddress(ip));
|
||||
}
|
||||
|
||||
const handleHighestIPChange = (ip) => {
|
||||
setHighestIP(cleanupIpAddress(ip));
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-ip-address-input">
|
||||
<div className="rsssl-ip-address-input__inner">
|
||||
<div className="rsssl-ip-address-input__icon"></div>
|
||||
<IpAddressInput
|
||||
id="lowestIP"
|
||||
type="text"
|
||||
className="rsssl-ip-address-input__input"
|
||||
value={lowestIP}
|
||||
onChange={ (e) => handleLowestIPChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="rsssl-ip-address-input__inner">
|
||||
<div className="rsssl-ip-address-input__icon"></div>
|
||||
<IpAddressInput
|
||||
id="highestIP"
|
||||
type="text"
|
||||
className="rsssl-ip-address-input__input"
|
||||
value={highestIP}
|
||||
onChange={(e) => handleHighestIPChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={'rsssl-container'}>
|
||||
<div className={'rsssl-container__inner'}>
|
||||
<button
|
||||
className={'button button--primary'}
|
||||
onClick={() => {
|
||||
validateIpRange(lowestIP, highestIP);
|
||||
}}
|
||||
>
|
||||
Validate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Cidr;
|
||||
@@ -0,0 +1,223 @@
|
||||
/* Creates A Store For Risk Data using Zustand */
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import {produce} from "immer";
|
||||
|
||||
const CountryDataTableStore = create((set, get) => ({
|
||||
|
||||
processing: false,
|
||||
dataLoaded: false,
|
||||
pagination: {},
|
||||
dataActions: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
sortColumn: 'country_name',
|
||||
sortDirection: 'asc',
|
||||
filterColumn: '',
|
||||
filterValue: '',
|
||||
search: '',
|
||||
searchColumns: ['country_name']
|
||||
},
|
||||
CountryDataTable: [],
|
||||
rowCleared: false,
|
||||
setDataActions: async (data) => {
|
||||
set(produce((state) => {
|
||||
state.dataActions = data;
|
||||
})
|
||||
);
|
||||
},
|
||||
fetchData: async (action, dataActions) => {
|
||||
//we check if the processing is already true, if so we return
|
||||
set({processing: true});
|
||||
set({dataLoaded: false});
|
||||
set({rowCleared: true});
|
||||
|
||||
if (Object.keys(dataActions).length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action,
|
||||
dataActions
|
||||
);
|
||||
//now we set the EventLog
|
||||
if (response && response.request_success) {
|
||||
set({CountryDataTable: response, dataLoaded: true, processing: false, pagination: response.pagination});
|
||||
}
|
||||
set({ rowCleared: true });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
set({processing: false});
|
||||
set({rowCleared: false});
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
handleCountryTableSearch: async (search, searchColumns) => {
|
||||
//Add the search to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, search, searchColumns};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
handleCountryTablePageChange: async (page, pageSize) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, page, pageSize};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
handleCountryTableRowsChange: async (currentRowsPerPage, currentPage) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, currentRowsPerPage, currentPage};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
//this handles all pagination and sorting
|
||||
handleCountryTableSort: async (column, sortDirection) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, sortColumn: column, sortDirection};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
handleCountryTableFilter: async (column, filterValue) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, filterColumn: column, filterValue};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
/*
|
||||
* This function add a new row to the table
|
||||
*/
|
||||
updateRow: async (value, status, dataActions) => {
|
||||
set({processing: true});
|
||||
let data = {
|
||||
value: value,
|
||||
status: status
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
'country_update_row',
|
||||
data
|
||||
);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
await get().fetchData('rsssl_limit_login_country', dataActions);
|
||||
// Potentially notify the user of success, if needed.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Handle any unsuccessful response if needed.
|
||||
return { success: false, message: response?.message || 'Failed to add country', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
updateRowRegion: async (value, status, dataActions) => {
|
||||
set({processing: true});
|
||||
let data = {
|
||||
value: value,
|
||||
status: status
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
'region_update_row',
|
||||
data
|
||||
);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
await get().fetchData('rsssl_limit_login_country', dataActions);
|
||||
// Potentially notify the user of success, if needed.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Handle any unsuccessful response if needed.
|
||||
return { success: false, message: response?.message || 'Failed to add region', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
|
||||
resetRegions: async (region, dataActions) => {
|
||||
set({processing: true});
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
'delete_entries_regions',
|
||||
{value: region}
|
||||
);
|
||||
//now we set the EventLog
|
||||
if (response && response.success) {
|
||||
await get().fetchData('rsssl_limit_login_country', dataActions);
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
return { success: false, message: response?.message || 'Failed to reset region', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
|
||||
resetRow: async (id, dataActions) => {
|
||||
set({processing: true});
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
'delete_entries',
|
||||
{id}
|
||||
);
|
||||
//now we set the EventLog
|
||||
if (response && response.success) {
|
||||
await get().fetchData('rsssl_limit_login_country', dataActions);
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
return { success: false, message: response?.message || 'Failed to reset country', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
|
||||
resetMultiRow: async (ids, dataActions) => {
|
||||
set({processing: true});
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
'delete_entries',
|
||||
{ids}
|
||||
);
|
||||
//now we set the EventLog
|
||||
if (response && response.success) {
|
||||
await get().fetchData('rsssl_limit_login_country', dataActions);
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
return { success: false, message: response?.message || 'Failed to reset country', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
export default CountryDataTableStore;
|
||||
@@ -0,0 +1,462 @@
|
||||
import {useEffect, useState, useCallback} from '@wordpress/element';
|
||||
import DataTable, {createTheme} from "react-data-table-component";
|
||||
import CountryDataTableStore from "./CountryDataTableStore";
|
||||
import EventLogDataTableStore from "../EventLog/EventLogDataTableStore";
|
||||
import FilterData from "../FilterData";
|
||||
import Flag from "../../utils/Flag/Flag";
|
||||
import {__} from '@wordpress/i18n';
|
||||
import useFields from "../FieldsData";
|
||||
import SearchBar from "../DynamicDataTable/SearchBar";
|
||||
import useMenu from "../../Menu/MenuData";
|
||||
|
||||
const CountryDatatable = (props) => {
|
||||
const {fieldAlreadyEnabled, getFieldValue, getField, showSavedSettingsNotice, saveFields, setHighLightField} = useFields();
|
||||
const {
|
||||
CountryDataTable,
|
||||
dataLoaded,
|
||||
fetchData,
|
||||
processing,
|
||||
handleCountryTableFilter,
|
||||
updateRow,
|
||||
pagination,
|
||||
handleCountryTablePageChange,
|
||||
handleCountryTableRowsChange,
|
||||
handleCountryTableSort,
|
||||
handleCountryTableSearch,
|
||||
addRegion,
|
||||
resetRegions,
|
||||
addRowMultiple,
|
||||
resetRow,
|
||||
resetMultiRow,
|
||||
updateRowRegion,
|
||||
dataActions,
|
||||
rowCleared,
|
||||
setDataActions,
|
||||
} = CountryDataTableStore();
|
||||
|
||||
const {setSelectedSubMenuItem} = useMenu();
|
||||
|
||||
const {
|
||||
fetchDynamicData,
|
||||
} = EventLogDataTableStore();
|
||||
|
||||
const {
|
||||
setSelectedFilter,
|
||||
getCurrentFilter,
|
||||
setProcessingFilter,
|
||||
} = FilterData();
|
||||
|
||||
const [rowsSelected, setRowsSelected] = useState([]);
|
||||
const moduleName = 'rsssl-group-filter-limit_login_attempts_country';
|
||||
const [tableHeight, setTableHeight] = useState(600); // Starting height
|
||||
const rowHeight = 50; // Height of each row.
|
||||
|
||||
useEffect(() => {
|
||||
const element = document.getElementById('set_to_captcha_configuration');
|
||||
const clickListener = async event => {
|
||||
event.preventDefault();
|
||||
if (element) {
|
||||
await redirectToAddCaptcha(element);
|
||||
}
|
||||
};
|
||||
|
||||
if (element) {
|
||||
element.addEventListener('click', clickListener);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (element) {
|
||||
element.removeEventListener('click', clickListener);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const redirectToAddCaptcha = async (element) => {
|
||||
// We fetch the props from the menu item
|
||||
let menuItem = getField('enabled_captcha_provider');
|
||||
|
||||
// Create a new object based on the menuItem, including the new property
|
||||
let highlightingMenuItem = {
|
||||
...menuItem,
|
||||
highlight_field_id: 'enabled_captcha_provider',
|
||||
};
|
||||
|
||||
setHighLightField(highlightingMenuItem.highlight_field_id);
|
||||
let highlightField = getField(highlightingMenuItem.highlight_field_id);
|
||||
await setSelectedSubMenuItem(highlightField.menu_id);
|
||||
}
|
||||
|
||||
const buildColumn = useCallback((column) => ({
|
||||
//if the filter is set to region and the columns = status we do not want to show the column
|
||||
omit: getCurrentFilter(moduleName) === 'regions' && column.column === 'status',
|
||||
name: column.name,
|
||||
sortable: column.sortable,
|
||||
searchable: column.searchable,
|
||||
width: column.width,
|
||||
visible: column.visible,
|
||||
column: column.column,
|
||||
selector: row => row[column.column],
|
||||
}), []);
|
||||
let field = props.field;
|
||||
const columns = field.columns.map(buildColumn);
|
||||
|
||||
const searchableColumns = columns
|
||||
.filter(column => column.searchable)
|
||||
.map(column => column.column);
|
||||
|
||||
useEffect(() => {
|
||||
const currentFilter = getCurrentFilter(moduleName);
|
||||
if (!currentFilter) {
|
||||
setSelectedFilter('blocked', moduleName);
|
||||
}
|
||||
setProcessingFilter(processing);
|
||||
handleCountryTableFilter('status', currentFilter);
|
||||
|
||||
}, [moduleName, handleCountryTableFilter, getCurrentFilter(moduleName), setSelectedFilter, CountryDatatable, processing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dataActions.filterColumn === 'status') {
|
||||
const {search, searchColumns, ...rest} = dataActions;
|
||||
setDataActions(rest);
|
||||
}
|
||||
}, [dataActions.filterColumn])
|
||||
|
||||
useEffect(() => {
|
||||
setRowsSelected([]);
|
||||
}, [CountryDataTable]);
|
||||
|
||||
//if the dataActions are changed, we fetch the data
|
||||
useEffect(() => {
|
||||
//we make sure the dataActions are changed in the store before we fetch the data
|
||||
if (dataActions) {
|
||||
fetchData(field.action, dataActions)
|
||||
}
|
||||
}, [dataActions.sortDirection, dataActions.filterValue, dataActions.search, dataActions.page, dataActions.currentRowsPerPage, fieldAlreadyEnabled('enable_limited_login_attempts')]);
|
||||
|
||||
let enabled = getFieldValue('enable_limited_login_attempts');
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
saveFields(false, false)
|
||||
};
|
||||
}, [enabled]);
|
||||
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light');
|
||||
|
||||
const handleSelection = useCallback((state) => {
|
||||
setRowsSelected(state.selectedRows);
|
||||
}, []);
|
||||
|
||||
const allowRegionByCode = useCallback(async (code, regionName = '') => {
|
||||
if (Array.isArray(code)) {
|
||||
const ids = code.map(item => item.id);
|
||||
const regions = code.map(item => item.iso2_code);
|
||||
let no_error = true;
|
||||
regions.forEach((code) => {
|
||||
resetRegions(code, dataActions).then(
|
||||
(response) => {
|
||||
if (!response.success) {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
no_error = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
if(no_error) {
|
||||
showSavedSettingsNotice(__('Selected regions are now allowed', 'really-simple-ssl'));
|
||||
}
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
await resetRegions(code, dataActions);
|
||||
showSavedSettingsNotice(__('%s is now allowed', 'really-simple-ssl')
|
||||
.replace('%s', regionName));
|
||||
}
|
||||
await fetchDynamicData('event_log');
|
||||
}, [resetRegions, getCurrentFilter(moduleName), dataActions]);
|
||||
|
||||
|
||||
const allowMultiple = useCallback((rows) => {
|
||||
const ids = rows.map(item => item.id);
|
||||
resetMultiRow(ids, dataActions).then((response) => {
|
||||
if (response && response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
});
|
||||
}, [resetMultiRow, getCurrentFilter(moduleName), dataActions]);
|
||||
|
||||
const allowById = useCallback((id) => {
|
||||
resetRow(id, dataActions).then(
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
}, [resetRow, getCurrentFilter(moduleName), dataActions]);
|
||||
|
||||
const blockRegionByCode = useCallback(async (code, region = '') => {
|
||||
if (Array.isArray(code)) {
|
||||
const ids = code.map(item => item.id);
|
||||
const regions = code.map(item => item.iso2_code);
|
||||
await updateRowRegion(regions, 'blocked', dataActions).then(
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
updateRowRegion(code, 'blocked', dataActions).then(
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await fetchDynamicData('event_log');
|
||||
|
||||
}, [addRegion, getCurrentFilter(moduleName), dataActions]);
|
||||
|
||||
const blockCountryByCode = useCallback(async (code) => {
|
||||
if (Array.isArray(code)) {
|
||||
const ids = code.map(item => item.iso2_code);
|
||||
|
||||
await updateRow(ids, 'blocked', dataActions).then(
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
await updateRow(code, 'blocked', dataActions).then(
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await fetchDynamicData('event_log');
|
||||
|
||||
}, [updateRow, addRowMultiple, dataActions, getCurrentFilter(moduleName)]);
|
||||
|
||||
const data = {...CountryDataTable.data};
|
||||
|
||||
const generateFlag = useCallback((flag, title) => (
|
||||
<>
|
||||
<Flag
|
||||
countryCode={flag}
|
||||
style={{
|
||||
fontSize: '2em',
|
||||
}}
|
||||
title={title}
|
||||
continent={(getCurrentFilter(moduleName) === 'regions')}
|
||||
/>
|
||||
</>
|
||||
), []);
|
||||
|
||||
const ActionButton = ({onClick, children, className}) => (
|
||||
// <div className={`rsssl-action-buttons__inner`}>
|
||||
<button
|
||||
className={`button ${className} rsssl-action-buttons__button`}
|
||||
onClick={onClick}
|
||||
disabled={processing}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
// </div>
|
||||
);
|
||||
|
||||
const generateActionButtons = useCallback((id, status, region_name, db_id ) => (
|
||||
<div className="rsssl-action-buttons">
|
||||
{getCurrentFilter(moduleName) === 'blocked' && (
|
||||
<ActionButton onClick={() => allowById(id)}
|
||||
className="button-secondary">
|
||||
{__("Allow", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
)}
|
||||
{getCurrentFilter(moduleName) === 'regions' && (
|
||||
<>
|
||||
<ActionButton
|
||||
onClick={() => blockRegionByCode(id, region_name)} className="button-primary">
|
||||
{__("Block", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={() => allowRegionByCode(id, region_name)} className="button-secondary">
|
||||
{__("Allow", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</>
|
||||
)}
|
||||
{getCurrentFilter(moduleName) === 'countries' && (
|
||||
<>
|
||||
{status === 'blocked' ? (
|
||||
<ActionButton
|
||||
onClick={() => allowById(db_id)} className="button-secondary">
|
||||
{__("Allow", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
) : (
|
||||
<ActionButton
|
||||
onClick={() => blockCountryByCode(id)} className="button-primary">
|
||||
{__("Block", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
), [getCurrentFilter, moduleName, allowById, blockRegionByCode, allowRegionByCode, blockCountryByCode]);
|
||||
|
||||
|
||||
for (const key in data) {
|
||||
const dataItem = {...data[key]};
|
||||
if (getCurrentFilter(moduleName) === 'regions' || getCurrentFilter(moduleName) === 'countries') {
|
||||
dataItem.action = generateActionButtons(dataItem.attempt_value, dataItem.status, dataItem.region, dataItem.db_id);
|
||||
} else {
|
||||
dataItem.action = generateActionButtons(dataItem.id);
|
||||
}
|
||||
dataItem.attempt_value = generateFlag(dataItem.attempt_value, dataItem.country_name);
|
||||
dataItem.status = __(dataItem.status = dataItem.status.charAt(0).toUpperCase() + dataItem.status.slice(1), 'really-simple-ssl');
|
||||
data[key] = dataItem;
|
||||
}
|
||||
|
||||
const options = Object.entries(props.field.options).map(([value, label]) => ({value, label}));
|
||||
|
||||
let paginationSet;
|
||||
paginationSet = typeof pagination !== 'undefined';
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(data).length === 0 ) {
|
||||
setTableHeight(100); // Adjust depending on your UI measurements
|
||||
} else {
|
||||
setTableHeight(rowHeight * (paginationSet ? pagination.perPage + 2 : 12)); // Adjust depending on your UI measurements
|
||||
}
|
||||
|
||||
}, [paginationSet, pagination?.perPage, data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-container">
|
||||
<div>
|
||||
{/* reserved for left side buttons */}
|
||||
</div>
|
||||
<SearchBar handleSearch={handleCountryTableSearch} searchableColumns={searchableColumns}/>
|
||||
</div>
|
||||
{rowsSelected.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
}}
|
||||
>
|
||||
<div className={"rsssl-multiselect-datatable-form rsssl-primary"}>
|
||||
<div>
|
||||
{__("You have selected %s rows", "really-simple-ssl").replace('%s', rowsSelected.length)}
|
||||
</div>
|
||||
<div className="rsssl-action-buttons">
|
||||
{getCurrentFilter(moduleName) === 'countries' && (
|
||||
<>
|
||||
<ActionButton
|
||||
onClick={() => blockCountryByCode(rowsSelected)} className="button-primary">
|
||||
{__("Block", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</>
|
||||
)}
|
||||
{getCurrentFilter(moduleName) === 'blocked' && (
|
||||
<ActionButton
|
||||
onClick={() => allowMultiple(rowsSelected)}>
|
||||
{__("Allow", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
)}
|
||||
{getCurrentFilter(moduleName) === 'regions' && (
|
||||
<>
|
||||
<ActionButton
|
||||
onClick={() => blockRegionByCode(rowsSelected)} className="button-primary">
|
||||
{__("Block", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={() => allowRegionByCode(rowsSelected)} className="button-secondary">
|
||||
{__("Allow", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={Object.values(data)}
|
||||
dense
|
||||
pagination={!processing}
|
||||
paginationServer
|
||||
paginationTotalRows={paginationSet ? pagination.totalRows : 10}
|
||||
paginationPerPage={paginationSet ? pagination.perPage : 10}
|
||||
paginationDefaultPage={paginationSet ? pagination.currentPage : 1}
|
||||
paginationComponentOptions={{
|
||||
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
|
||||
rangeSeparatorText: __('of', 'really-simple-ssl'),
|
||||
noRowsPerPage: false,
|
||||
selectAllRowsItem: false,
|
||||
selectAllRowsItemText: __('All', 'really-simple-ssl'),
|
||||
|
||||
}}
|
||||
onChangeRowsPerPage={handleCountryTableRowsChange}
|
||||
onChangePage={handleCountryTablePageChange}
|
||||
sortServer={!processing}
|
||||
onSort={handleCountryTableSort}
|
||||
paginationRowsPerPageOptions={[10, 25, 50, 100]}
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
selectableRows={!processing}
|
||||
clearSelectedRows={rowCleared}
|
||||
onSelectedRowsChange={handleSelection}
|
||||
theme="really-simple-plugins"
|
||||
customStyles={customStyles}
|
||||
/>
|
||||
{!enabled && (
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay"><span
|
||||
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Activate Limit login attempts to enable this block.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CountryDatatable;
|
||||
@@ -0,0 +1,416 @@
|
||||
/* Creates A Store For Risk Data using Zustand */
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import {produce} from "immer";
|
||||
|
||||
const IpAddressDataTableStore = create((set, get) => ({
|
||||
|
||||
processing: false,
|
||||
dataLoaded: false,
|
||||
ipAddress: '',
|
||||
highestIP: '',
|
||||
lowestIP: '',
|
||||
statusSelected: 'blocked',
|
||||
inputRangeValidated: false,
|
||||
cidr: '',
|
||||
ip_count: '',
|
||||
canSetCidr: false,
|
||||
ipRange: {},
|
||||
idSelected: '',
|
||||
pagination: {},
|
||||
dataActions: {},
|
||||
IpDataTable: [],
|
||||
maskError: false,
|
||||
rowCleared: false,
|
||||
|
||||
|
||||
setMaskError: (maskError) => {
|
||||
set({maskError});
|
||||
},
|
||||
|
||||
/*
|
||||
* This function fetches the data from the server and fills the property IpDataTable
|
||||
* Note this function works with the DataTable class on serverside
|
||||
*/
|
||||
fetchData: async (action, dataActions) => {
|
||||
set({processing: true});
|
||||
set({dataLoaded: false});
|
||||
set({rowCleared: true});
|
||||
//if the dataActions is empty we do nothing
|
||||
if (Object.keys(dataActions).length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action,
|
||||
dataActions
|
||||
);
|
||||
//now we set the EventLog
|
||||
if (response) {
|
||||
//if the response is empty we set the dummyData
|
||||
set({IpDataTable: response, dataLoaded: true, processing: false, pagination: response.pagination});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
set({processing: false});
|
||||
set({rowCleared: false});
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* This function handles the search, it is called from the search from it's parent class
|
||||
*/
|
||||
handleIpTableSearch: async (search, searchColumns) => {
|
||||
//Add the search to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, search, searchColumns};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
/*
|
||||
* This function handles the page change, it is called from the DataTable class
|
||||
*/
|
||||
handleIpTablePageChange: async (page, pageSize) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, page, pageSize};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
/*
|
||||
* This function handles the rows change, it is called from the DataTable class
|
||||
*/
|
||||
handleIpTableRowsChange: async (currentRowsPerPage, currentPage) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, currentRowsPerPage, currentPage};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
/*
|
||||
* This function handles the sort, it is called from the DataTable class
|
||||
*/
|
||||
handleIpTableSort: async (column, sortDirection) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, sortColumn: column, sortDirection};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
/*
|
||||
* This function handles the filter, it is called from the GroupSetting class
|
||||
*/
|
||||
handleIpTableFilter: async (column, filterValue) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, filterColumn: column, filterValue};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
/*
|
||||
* This function sets the ip address and is used by Cidr and IpAddressInput
|
||||
*/
|
||||
setIpAddress: (ipAddress) => {
|
||||
if(ipAddress.length === 0) {
|
||||
return;
|
||||
}
|
||||
let ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$|^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,4}|((25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9]))|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9]))$/;
|
||||
if (ipAddress.includes('/')) {
|
||||
let finalIp = '';
|
||||
// Split the input into IP and CIDR mask
|
||||
let [ip, mask] = ipAddress.split('/');
|
||||
//if , we change it to .
|
||||
ip = ip.replace(/,/g, '.');
|
||||
if (mask.length <= 0 ) {
|
||||
if (!ipRegex.test(ip)) {
|
||||
set({maskError: true});
|
||||
} else {
|
||||
set({maskError: false});
|
||||
}
|
||||
finalIp = `${ip}/${mask}`;
|
||||
} else {
|
||||
finalIp = mask ? `${ip}/${mask}` : ip;
|
||||
}
|
||||
set({ ipAddress: finalIp })
|
||||
} else {
|
||||
if (!ipRegex.test(ipAddress)) {
|
||||
set({maskError: true});
|
||||
} else {
|
||||
set({maskError: false});
|
||||
}
|
||||
set({ ipAddress: ipAddress.replace(/,/g, '.') })
|
||||
}
|
||||
},
|
||||
|
||||
resetRange: () => {
|
||||
set({inputRangeValidated: false});
|
||||
set({highestIP: ''});
|
||||
set({lowestIP: ''});
|
||||
set({ipAddress: ''});
|
||||
set({maskError: false});
|
||||
},
|
||||
|
||||
/*
|
||||
* This function sets the status selected and is used by Cidr and IpAddressInput and from the options
|
||||
*/
|
||||
setStatusSelected: (statusSelected) => {
|
||||
set({statusSelected});
|
||||
},
|
||||
|
||||
/*
|
||||
* This function sets the id selected and is used by Cidr and IpAddressInput and from the options
|
||||
*/
|
||||
setId: (idSelected) => {
|
||||
set({idSelected});
|
||||
},
|
||||
|
||||
/*
|
||||
* This function updates the row only changing the status
|
||||
*/
|
||||
/*
|
||||
* This function updates the row only changing the status
|
||||
*/
|
||||
updateRow: async (value, status, dataActions) => {
|
||||
set({processing: true});
|
||||
let data = {
|
||||
value: value,
|
||||
status: status
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
'ip_update_row',
|
||||
data
|
||||
);
|
||||
if (response && response.request_success) {
|
||||
await get().fetchData('rsssl_limit_login', dataActions);
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
return { success: false, message: response?.message || 'Failed to add ip', response };
|
||||
}
|
||||
} catch (e) {
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This function validates the ip address string if it is a proper ip address
|
||||
* This checks ipv4 addresses
|
||||
*
|
||||
* @param ip
|
||||
* @returns {boolean}
|
||||
*/
|
||||
validateIpv4: (ip) => {
|
||||
const parts = ip.split(".");
|
||||
if (parts.length !== 4) return false;
|
||||
for (let part of parts) {
|
||||
const num = parseInt(part, 10);
|
||||
if (isNaN(num) || num < 0 || num > 255) return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* This function validates the ip address string if it is a proper ip address
|
||||
* This checks ipv6 addresses
|
||||
*
|
||||
* @param ip
|
||||
* @returns {boolean}
|
||||
*/
|
||||
validateIpv6: (ip) => {
|
||||
const parts = ip.split(":");
|
||||
if (parts.length !== 8) return false;
|
||||
|
||||
for (let part of parts) {
|
||||
if (part.length > 4 || !/^[0-9a-fA-F]+$/.test(part)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
extendIpV6: (ip) => {
|
||||
// Handle the special case of '::' at the start or end
|
||||
if (ip === '::') ip = '0::0';
|
||||
|
||||
// Handle the '::' within the address
|
||||
if (ip.includes('::')) {
|
||||
const parts = ip.split('::');
|
||||
if (parts.length > 2) return false;
|
||||
|
||||
const left = parts[0].split(':').filter(Boolean);
|
||||
const right = parts[1].split(':').filter(Boolean);
|
||||
|
||||
// Calculate how many zeros are needed
|
||||
const zerosNeeded = 8 - (left.length + right.length);
|
||||
|
||||
// Concatenate all parts with the appropriate number of zeros
|
||||
return [...left, ...Array(zerosNeeded).fill('0'), ...right].join(':');
|
||||
}
|
||||
return ip;
|
||||
},
|
||||
|
||||
/**
|
||||
* This function converts the ip address to a number
|
||||
*
|
||||
* @param ip
|
||||
* @returns {*}
|
||||
*/
|
||||
ipToNumber: (ip) => {
|
||||
if (get().validateIpv4(ip)) {
|
||||
return get().ipV4ToNumber(ip);
|
||||
} else if (get().validateIpv6(get().extendIpV6(ip))) {
|
||||
return get().ipV6ToNumber(get().extendIpV6(ip));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This function converts the ip address to a number if it is a ipv4 address
|
||||
* @param ip
|
||||
* @returns {*}
|
||||
*/
|
||||
ipV4ToNumber: (ip) => {
|
||||
return ip.split(".").reduce((acc, cur) => (acc * 256 + parseInt(cur, 10)) >>> 0, 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* This function converts the ip address to a number if it is a ipv6 address
|
||||
* @param ip
|
||||
* @returns {*}
|
||||
*/
|
||||
ipV6ToNumber: (ip) => {
|
||||
return ip.split(":").reduce((acc, cur) => {
|
||||
const segmentValue = parseInt(cur, 16);
|
||||
if (isNaN(segmentValue)) {
|
||||
console.warn(`Invalid segment in IPv6 address: ${oldIp}`);
|
||||
return acc;
|
||||
}
|
||||
return (acc << BigInt(16)) + BigInt(segmentValue);
|
||||
}, BigInt(0));
|
||||
},
|
||||
|
||||
/**
|
||||
* This function validates the ip range, if the lowest is lower than the highest
|
||||
* This checks ipv4 and ipv6 addresses
|
||||
*
|
||||
* @param lowest
|
||||
* @param highest
|
||||
*/
|
||||
validateIpRange: (lowest, highest) => {
|
||||
set({inputRangeValidated: false});
|
||||
let from = '';
|
||||
let to = '';
|
||||
//first we determine if the IP is ipv4 or ipv6
|
||||
if (lowest && highest) {
|
||||
if (get().validateIpv4(lowest) && get().validateIpv4(highest)) {
|
||||
//now we check if the lowest is lower than the highest
|
||||
if (get().ipToNumber(lowest) > get().ipToNumber(highest)) {
|
||||
console.warn('lowest is higher than highest');
|
||||
set({inputRangeValidated: false});
|
||||
return;
|
||||
}
|
||||
from = lowest;
|
||||
to = highest;
|
||||
set({inputRangeValidated: true});
|
||||
} else if (get().validateIpv6(get().extendIpV6(lowest)) && get().validateIpv6(get().extendIpV6(highest))) {
|
||||
//now we check if the lowest is lower than the highest
|
||||
if (get().ipToNumber(get().extendIpV6(lowest)) > get().ipToNumber(get().extendIpV6(highest))) {
|
||||
console.warn('lowest is higher than highest');
|
||||
set({inputRangeValidated: false});
|
||||
return;
|
||||
}
|
||||
from = get().extendIpV6(lowest);
|
||||
to = get().extendIpV6(highest);
|
||||
set({inputRangeValidated: true});
|
||||
}
|
||||
}
|
||||
if (get().inputRangeValidated) {
|
||||
let lowest = from;
|
||||
let highest = to;
|
||||
set({ipRange: {lowest, highest}});
|
||||
get().fetchCidrData('get_mask_from_range');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This function fetches the cidr data from the server and sets the cidr and ip_count
|
||||
* This function is called from the Cidr class
|
||||
*
|
||||
* @param action
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
fetchCidrData: async (action) => {
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action,
|
||||
get().ipRange
|
||||
);
|
||||
//now we set the EventLog
|
||||
if (response) {
|
||||
//we set the cidrFound and cidrCount
|
||||
set({cidr: response.cidr, ipAddress: response.cidr, ip_count: response.ip_count, canSetCidr: true});
|
||||
//we reload the event log
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
|
||||
resetRow: async (id, dataActions) => {
|
||||
set({processing: true});
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
'delete_entries',
|
||||
{id}
|
||||
);
|
||||
//now we set the EventLog
|
||||
if (response && response.success) {
|
||||
await get().fetchData('rsssl_limit_login', dataActions);
|
||||
// Return the success message from the API response.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Return a custom error message or the API response message.
|
||||
return { success: false, message: response?.message || 'Failed to reset ip', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Return the caught error with a custom message.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
|
||||
resetMultiRow: async (ids, dataActions) => {
|
||||
set({processing: true});
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
'delete_entries',
|
||||
{ids}
|
||||
);
|
||||
//now we set the EventLog
|
||||
if (response && response.success) {
|
||||
if (response.success) {
|
||||
await get().fetchData('rsssl_limit_login', dataActions);
|
||||
return {success: true, message: response.message, response};
|
||||
} else
|
||||
return {success: false, message: response?.message || 'Failed to reset ip', response};
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
export default IpAddressDataTableStore;
|
||||
@@ -0,0 +1,328 @@
|
||||
import {__} from '@wordpress/i18n';
|
||||
import {useEffect, useState, useCallback} from '@wordpress/element';
|
||||
import DataTable, {createTheme} from "react-data-table-component";
|
||||
import IpAddressDataTableStore from "./IpAddressDataTableStore";
|
||||
import EventLogDataTableStore from "../EventLog/EventLogDataTableStore";
|
||||
import FilterData from "../FilterData";
|
||||
import Flag from "../../utils/Flag/Flag";
|
||||
import AddIpAddressModal from "./AddIpAddressModal";
|
||||
import useFields from "../FieldsData";
|
||||
import FieldsData from "../FieldsData";
|
||||
import SearchBar from "../DynamicDataTable/SearchBar";
|
||||
import AddButton from "../DynamicDataTable/AddButton";
|
||||
|
||||
const IpAddressDatatable = (props) => {
|
||||
const {
|
||||
IpDataTable,
|
||||
dataLoaded,
|
||||
dataActions,
|
||||
handleIpTableRowsChange,
|
||||
fetchData,
|
||||
handleIpTableSort,
|
||||
handleIpTablePageChange,
|
||||
handleIpTableSearch,
|
||||
handleIpTableFilter,
|
||||
ipAddress,
|
||||
updateRow,
|
||||
pagination,
|
||||
resetRow,
|
||||
resetMultiRow,
|
||||
setStatusSelected,
|
||||
rowCleared,
|
||||
processing
|
||||
} = IpAddressDataTableStore()
|
||||
|
||||
const {
|
||||
fetchDynamicData,
|
||||
} = EventLogDataTableStore();
|
||||
|
||||
//here we set the selectedFilter from the Settings group
|
||||
const {
|
||||
setSelectedFilter,
|
||||
getCurrentFilter,
|
||||
setProcessingFilter,
|
||||
} = FilterData();
|
||||
|
||||
const [addingIpAddress, setAddingIpAddress] = useState(false);
|
||||
const [rowsSelected, setRowsSelected] = useState([]);
|
||||
const {fieldAlreadyEnabled, getFieldValue} = useFields();
|
||||
const {showSavedSettingsNotice} = FieldsData();
|
||||
const [tableHeight, setTableHeight] = useState(600); // Starting height
|
||||
const rowHeight = 50; // Height of each row.
|
||||
|
||||
const moduleName = 'rsssl-group-filter-limit_login_attempts_ip_address';
|
||||
|
||||
const buildColumn = useCallback((column) => ({
|
||||
name: column.name,
|
||||
sortable: column.sortable,
|
||||
searchable: column.searchable,
|
||||
width: column.width,
|
||||
visible: column.visible,
|
||||
column: column.column,
|
||||
selector: row => row[column.column],
|
||||
}), []);
|
||||
//getting the fields from the props
|
||||
let field = props.field;
|
||||
//we loop through the fields
|
||||
const columns = field.columns.map(buildColumn);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const currentFilter = getCurrentFilter(moduleName);
|
||||
if (!currentFilter) {
|
||||
setSelectedFilter('locked', moduleName);
|
||||
}
|
||||
setProcessingFilter(processing);
|
||||
handleIpTableFilter('status', currentFilter);
|
||||
}, [moduleName, handleIpTableFilter, getCurrentFilter(moduleName), setSelectedFilter, IpDataTable, processing]);
|
||||
|
||||
useEffect(() => {
|
||||
setRowsSelected([]);
|
||||
}, [IpDataTable]);
|
||||
|
||||
//if the dataActions are changed, we fetch the data
|
||||
useEffect(() => {
|
||||
//we make sure the dataActions are changed in the store before we fetch the data
|
||||
if (dataActions) {
|
||||
fetchData(field.action, dataActions);
|
||||
}
|
||||
}, [dataActions.sortDirection, dataActions.filterValue, dataActions.search, dataActions.page, dataActions.currentRowsPerPage, fieldAlreadyEnabled('enable_limited_login_attempts')]);
|
||||
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for head cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for data cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light');
|
||||
|
||||
|
||||
let enabled = getFieldValue('enable_limited_login_attempts');
|
||||
|
||||
const handleOpen = () => {
|
||||
setAddingIpAddress(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAddingIpAddress(false);
|
||||
};
|
||||
|
||||
let searchableColumns = [];
|
||||
//setting the searchable columns
|
||||
columns.map(column => {
|
||||
if (column.searchable) {
|
||||
searchableColumns.push(column.column);
|
||||
}
|
||||
});
|
||||
|
||||
//now we get the options for the select control
|
||||
let options = props.field.options;
|
||||
//we divide the key into label and the value into value
|
||||
options = Object.entries(options).map((item) => {
|
||||
return {label: item[1], value: item[0]};
|
||||
});
|
||||
|
||||
|
||||
function handleStatusChange(value, id) {
|
||||
//if the id is not 'new' we update the row
|
||||
if (id !== 'new') {
|
||||
updateRow(id, value);
|
||||
} else {
|
||||
//if the id is 'new' we set the statusSelected
|
||||
setStatusSelected(value);
|
||||
}
|
||||
}
|
||||
|
||||
//we convert the data to an array
|
||||
let data = Object.values({...IpDataTable.data});
|
||||
const resetIpAddresses = useCallback(async (data) => {
|
||||
if (Array.isArray(data)) {
|
||||
const ids = data.map((item) => item.id);
|
||||
await resetMultiRow(ids, dataActions).then((response) => {
|
||||
if (response && response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message);
|
||||
}
|
||||
});
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
await resetRow(data, dataActions).then((response) => {
|
||||
if (response && response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
await fetchDynamicData('event_log')
|
||||
}, [resetMultiRow, resetRow, fetchDynamicData, dataActions]);
|
||||
|
||||
const ActionButton = ({onClick, children, className}) => (
|
||||
<div className={`rsssl-action-buttons__inner`}>
|
||||
<button
|
||||
className={`button ${className} rsssl-action-buttons__button`}
|
||||
onClick={onClick}
|
||||
disabled={processing}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
function generateActionbuttons(id) {
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-action-buttons">
|
||||
<ActionButton
|
||||
className="button-red"
|
||||
onClick={() => {
|
||||
resetIpAddresses(id);
|
||||
}}>
|
||||
{__("Delete", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
for (const key in data) {
|
||||
let dataItem = {...data[key]}
|
||||
|
||||
dataItem.action = generateActionbuttons(dataItem.id);
|
||||
dataItem.status = __(dataItem.status = dataItem.status.charAt(0).toUpperCase() + dataItem.status.slice(1), 'really-simple-ssl');
|
||||
|
||||
data[key] = dataItem;
|
||||
}
|
||||
|
||||
function handleSelection(state) {
|
||||
setRowsSelected(state.selectedRows);
|
||||
}
|
||||
|
||||
let paginationSet;
|
||||
paginationSet = typeof pagination !== 'undefined';
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(data).length === 0 ) {
|
||||
setTableHeight(100); // Adjust depending on your UI measurements
|
||||
} else {
|
||||
setTableHeight(rowHeight * (paginationSet ? pagination.perPage + 2 : 12)); // Adjust depending on your UI measurements
|
||||
}
|
||||
|
||||
}, [paginationSet, pagination?.perPage, data]);
|
||||
let debounceTimer;
|
||||
|
||||
return (
|
||||
<>
|
||||
<AddIpAddressModal
|
||||
isOpen={addingIpAddress}
|
||||
onRequestClose={handleClose}
|
||||
options={options}
|
||||
value={ipAddress}
|
||||
status={getCurrentFilter(moduleName)}
|
||||
dataActions={dataActions}
|
||||
>
|
||||
</AddIpAddressModal>
|
||||
<div className="rsssl-container">
|
||||
{/*display the add button on left side*/}
|
||||
<AddButton
|
||||
getCurrentFilter={getCurrentFilter}
|
||||
moduleName={moduleName}
|
||||
handleOpen={handleOpen}
|
||||
processing={processing}
|
||||
blockedText={__("Block IP Address", "really-simple-ssl")}
|
||||
allowedText={__("Trust IP Address", "really-simple-ssl")}
|
||||
/>
|
||||
|
||||
{/*Display the search bar*/}
|
||||
<SearchBar
|
||||
handleSearch={handleIpTableSearch}
|
||||
searchableColumns={searchableColumns}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ /*Display the action form what to do with the selected*/}
|
||||
{rowsSelected.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
}}>
|
||||
<div className={"rsssl-multiselect-datatable-form rsssl-primary"}
|
||||
>
|
||||
<div>
|
||||
{__("You have selected %s rows", "really-simple-ssl").replace('%s', rowsSelected.length)}
|
||||
</div>
|
||||
|
||||
<div className="rsssl-action-buttons">
|
||||
<ActionButton
|
||||
className="button-red"
|
||||
onClick={() => {
|
||||
resetIpAddresses(rowsSelected);
|
||||
}}
|
||||
>
|
||||
{__("Delete", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/*Display the datatable*/}
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={processing ? [] : data}
|
||||
dense
|
||||
paginationServer
|
||||
paginationTotalRows={paginationSet? pagination.totalRows: 10}
|
||||
paginationPerPage={paginationSet? pagination.perPage: 10}
|
||||
paginationDefaultPage={paginationSet?pagination.currentPage: 1}
|
||||
paginationComponentOptions={{
|
||||
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
|
||||
rangeSeparatorText: __('of', 'really-simple-ssl'),
|
||||
noRowsPerPage: false,
|
||||
selectAllRowsItem: false,
|
||||
selectAllRowsItemText: __('All', 'really-simple-ssl'),
|
||||
|
||||
}}
|
||||
loading={dataLoaded}
|
||||
pagination={!processing}
|
||||
onChangeRowsPerPage={handleIpTableRowsChange}
|
||||
onChangePage={handleIpTablePageChange}
|
||||
sortServer={!processing}
|
||||
onSort={handleIpTableSort}
|
||||
paginationRowsPerPageOptions={[10, 25, 50, 100]}
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
selectableRows={!processing}
|
||||
onSelectedRowsChange={handleSelection}
|
||||
clearSelectedRows={rowCleared}
|
||||
theme="really-simple-plugins"
|
||||
customStyles={customStyles}
|
||||
></DataTable>
|
||||
{!enabled && (
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay"><span
|
||||
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Activate Limit login attempts to enable this block.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default IpAddressDatatable;
|
||||
@@ -0,0 +1,45 @@
|
||||
import {useState} from '@wordpress/element';
|
||||
import {__} from "@wordpress/i18n";
|
||||
import Icon from "../../utils/Icon";
|
||||
import IpAddressDataTableStore from "./IpAddressDataTableStore";
|
||||
|
||||
|
||||
/**
|
||||
* Visual aid for adding an IP address to the list of blocked IP addresses
|
||||
*
|
||||
* @param props
|
||||
* @returns {*}
|
||||
* @constructor
|
||||
*/
|
||||
const IpAddressInput = (props) => {
|
||||
|
||||
const [value, setValue] = useState("");
|
||||
const [error, setError] = useState(false);
|
||||
const {maskError, setMaskError} = IpAddressDataTableStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.label &&
|
||||
<label
|
||||
htmlFor={props.id}
|
||||
className="rsssl-label"
|
||||
>{props.label}</label>
|
||||
}
|
||||
<br></br>
|
||||
<div className="input-container">
|
||||
<input
|
||||
type="text"
|
||||
id={props.id}
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
className={`rsssl-input full ${maskError ? 'rsssl-error' : 'rsssl-success'}`}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
</div>
|
||||
{maskError && <span
|
||||
style={{color: 'red', marginLeft: '10px'}}>{__('Invalid ip address', 'really-simple-ssl')}</span>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default IpAddressInput;
|
||||
@@ -0,0 +1,25 @@
|
||||
/* Creates A Store For Risk Data using Zustand */
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import {produce} from "immer";
|
||||
import React from "react";
|
||||
|
||||
const LimitLoginAttemptsData = create((set, get) => ({
|
||||
|
||||
processing:false,
|
||||
dataLoaded: false,
|
||||
EventLog: [],
|
||||
|
||||
fetchEventLog: async (selectedFilter) => {
|
||||
set({processing:true});
|
||||
try {
|
||||
let response = await rsssl_api.doAction(selectedFilter);
|
||||
set({EventLog: response, dataLoaded: true, processing:false});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
export default LimitLoginAttemptsData;
|
||||
@@ -0,0 +1,175 @@
|
||||
/* Creates A Store For Risk Data using Zustand */
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import {produce} from "immer";
|
||||
import React from "react";
|
||||
|
||||
const UserDataTableStore = create((set, get) => ({
|
||||
|
||||
processing: false,
|
||||
dataLoaded: false,
|
||||
pagination: {},
|
||||
dataActions: {},
|
||||
UserDataTable: [],
|
||||
rowCleared: false,
|
||||
|
||||
fetchData: async (action, dataActions) => {
|
||||
//we check if the processing is already true, if so we return
|
||||
set({processing: true});
|
||||
set({dataLoaded: false});
|
||||
set({rowCleared: true});
|
||||
if (Object.keys(dataActions).length === 0) {
|
||||
let dataActions = get().dataActions;
|
||||
}
|
||||
|
||||
if ( !get().processing ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(dataActions).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
//we empty all existing data
|
||||
set({UserDataTable: []});
|
||||
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action,
|
||||
dataActions
|
||||
);
|
||||
//now we set the EventLog
|
||||
//now we set the EventLog
|
||||
if (response && response.request_success) {
|
||||
set({UserDataTable: response, dataLoaded: true, processing: false, pagination: response.pagination});
|
||||
}
|
||||
set({ rowCleared: true });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
set({processing: false});
|
||||
set({rowCleared: false});
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
handleUserTableSearch: async (search, searchColumns) => {
|
||||
//Add the search to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, search, searchColumns};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
handleUserTablePageChange: async (page, pageSize) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, page, pageSize};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
handleUserTableRowsChange: async (currentRowsPerPage, currentPage) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, currentRowsPerPage, currentPage};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
//this handles all pagination and sorting
|
||||
handleUserTableSort: async (column, sortDirection) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, sortColumn: column, sortDirection};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
handleUserTableFilter: async (column, filterValue) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, filterColumn: column, filterValue};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
/*
|
||||
* This function updates the row only changing the status
|
||||
*/
|
||||
updateRow: async (value, status, dataActions) => {
|
||||
set({processing: true});
|
||||
let data = {
|
||||
value: value,
|
||||
status: status
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
'user_update_row',
|
||||
data
|
||||
);
|
||||
if (response && response.request_success) {
|
||||
await get().fetchData('rsssl_limit_login_user', dataActions);
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
return { success: false, message: response?.message || 'Failed to add user', response };
|
||||
}
|
||||
} catch (e) {
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
|
||||
resetRow: async (id, dataActions) => {
|
||||
set({processing: true});
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
'delete_entries',
|
||||
{id}
|
||||
);
|
||||
//now we set the EventLog
|
||||
if (response && response.success) {
|
||||
await get().fetchData('rsssl_limit_login_user', dataActions);
|
||||
// Return the success message from the API response.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Return a custom error message or the API response message.
|
||||
return { success: false, message: response?.message || 'Failed to reset user', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Return the caught error with a custom message.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
|
||||
resetMultiRow: async (ids, dataActions) => {
|
||||
set({processing: true});
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
'delete_entries',
|
||||
{ids}
|
||||
);
|
||||
console.error(response);
|
||||
//now we set the EventLog
|
||||
if (response && response.success) {
|
||||
if (response.success) {
|
||||
await get().fetchUserData('rsssl_limit_login_user', dataActions);
|
||||
return {success: true, message: response.message, response};
|
||||
} else
|
||||
return {success: false, message: response?.message || 'Failed to reset user', response};
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
export default UserDataTableStore;
|
||||
@@ -0,0 +1,291 @@
|
||||
import {__} from '@wordpress/i18n';
|
||||
import {useCallback, useEffect, useState} from '@wordpress/element';
|
||||
import DataTable, {createTheme} from "react-data-table-component";
|
||||
import UserDataTableStore from "./UserDataTableStore";
|
||||
import FilterData from "../FilterData";
|
||||
|
||||
import {button} from "@wordpress/components";
|
||||
import {produce} from "immer";
|
||||
import AddIpAddressModal from "./AddIpAddressModal";
|
||||
import AddUserModal from "./AddUserModal";
|
||||
import EventLogDataTableStore from "../EventLog/EventLogDataTableStore";
|
||||
import useFields from "../FieldsData";
|
||||
import FieldsData from "../FieldsData";
|
||||
import SearchBar from "../DynamicDataTable/SearchBar";
|
||||
import AddButton from "../DynamicDataTable/AddButton";
|
||||
|
||||
const UserDatatable = (props) => {
|
||||
let {
|
||||
UserDataTable,
|
||||
dataLoaded,
|
||||
fetchData,
|
||||
processing,
|
||||
handleUserTableFilter,
|
||||
handleUserTablePageChange,
|
||||
pagination,
|
||||
resetRow,
|
||||
resetMultiRow,
|
||||
dataActions,
|
||||
handleUserTableRowsChange,
|
||||
handleUserTableSort,
|
||||
handleUserTableSearch,
|
||||
updateMultiRow,
|
||||
updateRow,
|
||||
rowCleared
|
||||
} = UserDataTableStore()
|
||||
const {showSavedSettingsNotice} = FieldsData();
|
||||
const {
|
||||
fetchDynamicData,
|
||||
} = EventLogDataTableStore();
|
||||
//here we set the selectedFilter from the Settings group
|
||||
const {
|
||||
selectedFilter,
|
||||
setSelectedFilter,
|
||||
activeGroupId,
|
||||
getCurrentFilter,
|
||||
setProcessingFilter,
|
||||
} = FilterData();
|
||||
|
||||
const [rowsSelected, setRowsSelected] = useState([]);
|
||||
const [addingUser, setAddingUser] = useState(false);
|
||||
const [user, setUser] = useState('');
|
||||
const [tableHeight, setTableHeight] = useState(600); // Starting height
|
||||
const rowHeight = 50; // Height of each row.
|
||||
|
||||
const moduleName = 'rsssl-group-filter-limit_login_attempts_users';
|
||||
const {fields, fieldAlreadyEnabled, getFieldValue, saveFields} = useFields();
|
||||
|
||||
const buildColumn = useCallback((column) => ({
|
||||
name: column.name,
|
||||
sortable: column.sortable,
|
||||
searchable: column.searchable,
|
||||
width: column.width,
|
||||
visible: column.visible,
|
||||
column: column.column,
|
||||
selector: row => row[column.column],
|
||||
}), []);
|
||||
//getting the fields from the props
|
||||
let field = props.field;
|
||||
//we loop through the fields
|
||||
const columns = field.columns.map(buildColumn);
|
||||
|
||||
const searchableColumns = columns
|
||||
.filter(column => column.searchable)
|
||||
.map(column => column.column);
|
||||
|
||||
useEffect(() => {
|
||||
const currentFilter = getCurrentFilter(moduleName);
|
||||
if (!currentFilter) {
|
||||
setSelectedFilter('locked', moduleName);
|
||||
}
|
||||
setProcessingFilter(processing);
|
||||
handleUserTableFilter('status', currentFilter);
|
||||
}, [moduleName, handleUserTableFilter, getCurrentFilter(moduleName), setSelectedFilter, UserDatatable, processing]);
|
||||
|
||||
useEffect(() => {
|
||||
setRowsSelected([]);
|
||||
}, [UserDataTable]);
|
||||
|
||||
//if the dataActions are changed, we fetch the data
|
||||
useEffect(() => {
|
||||
//we make sure the dataActions are changed in the store before we fetch the data
|
||||
if (dataActions) {
|
||||
fetchData(field.action, dataActions)
|
||||
}
|
||||
}, [dataActions.sortDirection, dataActions.filterValue, dataActions.search, dataActions.page, dataActions.currentRowsPerPage, fieldAlreadyEnabled('enable_limited_login_attempts')]);
|
||||
|
||||
let enabled = getFieldValue('enable_limited_login_attempts');
|
||||
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for head cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for data cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light');
|
||||
|
||||
const handleOpen = () => {
|
||||
setAddingUser(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAddingUser(false);
|
||||
};
|
||||
|
||||
//now we get the options for the select control
|
||||
let options = props.field.options;
|
||||
//we divide the key into label and the value into value
|
||||
options = Object.entries(options).map((item) => {
|
||||
return {label: item[1], value: item[0]};
|
||||
});
|
||||
|
||||
const resetUsers = useCallback(async (data) => {
|
||||
if (Array.isArray(data)) {
|
||||
const ids = data.map((item) => item.id);
|
||||
await resetMultiRow(ids, dataActions).then((response) => {
|
||||
if (response && response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
});
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
await resetRow(data, dataActions).then((response) => {
|
||||
if (response && response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
await fetchDynamicData('event_log');
|
||||
}, [resetMultiRow, resetRow, fetchDynamicData, dataActions]);
|
||||
|
||||
const handleSelection = useCallback((state) => {
|
||||
setRowsSelected(state.selectedRows);
|
||||
}, []);
|
||||
|
||||
const ActionButton = ({onClick, children, className}) => (
|
||||
<div className={`rsssl-action-buttons__inner`}>
|
||||
<button
|
||||
className={`button ${className} rsssl-action-buttons__button`}
|
||||
onClick={onClick}
|
||||
disabled={processing}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const generateActionButtons = useCallback((id, status, region_name) => (
|
||||
<div className="rsssl-action-buttons">
|
||||
<ActionButton onClick={() => {
|
||||
resetUsers(id);
|
||||
}}
|
||||
className="button-red">
|
||||
{__("Delete", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</div>
|
||||
), [getCurrentFilter(moduleName), moduleName, resetUsers]);
|
||||
|
||||
|
||||
//we convert the data to an array
|
||||
let data = {...UserDataTable.data};
|
||||
|
||||
for (const key in data) {
|
||||
let dataItem = {...data[key]}
|
||||
//we log the dataItem
|
||||
//we add the action buttons
|
||||
dataItem.action = generateActionButtons(dataItem.id);
|
||||
dataItem.status = __(dataItem.status = dataItem.status.charAt(0).toUpperCase() + dataItem.status.slice(1), 'really-simple-ssl');
|
||||
data[key] = dataItem;
|
||||
}
|
||||
|
||||
let paginationSet;
|
||||
paginationSet = typeof pagination !== 'undefined';
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(data).length === 0 ) {
|
||||
setTableHeight(100); // Adjust depending on your UI measurements
|
||||
} else {
|
||||
setTableHeight(rowHeight * (paginationSet ? pagination.perPage + 2 : 12)); // Adjust depending on your UI measurements
|
||||
}
|
||||
|
||||
}, [paginationSet, pagination?.perPage, data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AddUserModal
|
||||
isOpen={addingUser}
|
||||
onRequestClose={handleClose}
|
||||
options={options}
|
||||
value={user}
|
||||
status={getCurrentFilter(moduleName)}
|
||||
dataActions={dataActions}
|
||||
>
|
||||
</AddUserModal>
|
||||
<div className="rsssl-container">
|
||||
{/*display the add button on left side*/}
|
||||
<AddButton
|
||||
getCurrentFilter={getCurrentFilter}
|
||||
moduleName={moduleName}
|
||||
handleOpen={handleOpen}
|
||||
processing={processing}
|
||||
blockedText={__("Block Username", "really-simple-ssl")}
|
||||
allowedText={__("Trust Username", "really-simple-ssl")}
|
||||
/>
|
||||
{/*Display the search bar*/}
|
||||
<SearchBar handleSearch={handleUserTableSearch} searchableColumns={searchableColumns}/>
|
||||
</div>
|
||||
{ /*Display the action form what to do with the selected*/}
|
||||
{rowsSelected.length > 0 && (
|
||||
<div style={{
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
}}>
|
||||
<div className={"rsssl-multiselect-datatable-form rsssl-primary"}
|
||||
>
|
||||
<div>
|
||||
{__("You have selected %s rows", "really-simple-ssl").replace('%s', rowsSelected.length)}
|
||||
</div>
|
||||
|
||||
<div className="rsssl-action-buttons">
|
||||
{/* if the id is new we show the Delete button */}
|
||||
<ActionButton
|
||||
className="button button-red rsssl-action-buttons__button"
|
||||
onClick={() => {resetUsers(rowsSelected)}}>
|
||||
{__("Delete", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/*Display the datatable*/}
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={processing && !dataLoaded? [] : Object.values(data)}
|
||||
dense
|
||||
pagination={!processing}
|
||||
paginationServer
|
||||
paginationTotalRows={paginationSet? pagination.totalRows: 10}
|
||||
onChangeRowsPerPage={handleUserTableRowsChange}
|
||||
onChangePage={handleUserTablePageChange}
|
||||
sortServer={!processing}
|
||||
onSort={handleUserTableSort}
|
||||
paginationRowsPerPageOptions={[10, 25, 50, 100]}
|
||||
selectableRows={!processing}
|
||||
onSelectedRowsChange={handleSelection}
|
||||
clearSelectedRows={rowCleared}
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
theme="really-simple-plugins"
|
||||
customStyles={customStyles}
|
||||
></DataTable>
|
||||
{!enabled && (
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay"><span
|
||||
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Activate Limit login attempts to enable this block.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
export default UserDatatable;
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
|
||||
const UseMixedContent = create(( set, get ) => ({
|
||||
mixedContentData: [],
|
||||
dataLoaded:false,
|
||||
fixedItemId:false,
|
||||
action:'',
|
||||
nonce:'',
|
||||
completedStatus:'never',
|
||||
progress:0,
|
||||
scanStatus:false,
|
||||
fetchMixedContentData: async () => {
|
||||
set({ scanStatus: 'running' } );
|
||||
const {data, progress, state, action, nonce, completed_status } = await getScanIteration(false);
|
||||
set({
|
||||
scanStatus: state,
|
||||
mixedContentData: data,
|
||||
progress: progress,
|
||||
action: action,
|
||||
nonce: nonce,
|
||||
completedStatus: completed_status,
|
||||
dataLoaded: true,
|
||||
});
|
||||
},
|
||||
start: async () => {
|
||||
const {data, progress, state, action, nonce, completed_status } = await getScanIteration('start');
|
||||
set({
|
||||
scanStatus: state,
|
||||
mixedContentData: data,
|
||||
progress: progress,
|
||||
action: action,
|
||||
nonce: nonce,
|
||||
completedStatus: completed_status,
|
||||
dataLoaded:true,
|
||||
});
|
||||
},
|
||||
runScanIteration: async () => {
|
||||
let currentState = get().scanStatus;
|
||||
if ( currentState==='stop' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {data, progress, state, action, nonce, completed_status } = await getScanIteration(currentState);
|
||||
if ( get().scanStatus !== 'stop' ) {
|
||||
set({
|
||||
scanStatus: state,
|
||||
mixedContentData: data,
|
||||
progress: progress,
|
||||
action: action,
|
||||
nonce: nonce,
|
||||
completedStatus: completed_status,
|
||||
dataLoaded:true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
stop: async () => {
|
||||
set({ scanStatus: 'stop' } );
|
||||
const {data, progress, state, action, nonce, completed_status } = await getScanIteration('stop');
|
||||
set({
|
||||
scanStatus: 'stop',
|
||||
mixedContentData: data,
|
||||
progress: progress,
|
||||
action: action,
|
||||
nonce: nonce,
|
||||
completedStatus: completed_status,
|
||||
});
|
||||
},
|
||||
removeDataItem: (removeItem) => {
|
||||
let data = get().mixedContentData;
|
||||
for (const item of data) {
|
||||
if (item.id===removeItem.id){
|
||||
item.fixed = true;
|
||||
}
|
||||
}
|
||||
set({
|
||||
mixedContentData: data,
|
||||
});
|
||||
},
|
||||
ignoreDataItem: (ignoreItem) => {
|
||||
let data = get().mixedContentData;
|
||||
for (const item of data) {
|
||||
if (item.id===ignoreItem.id){
|
||||
item.ignored = true;
|
||||
}
|
||||
}
|
||||
set({
|
||||
mixedContentData: data,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}));
|
||||
|
||||
export default UseMixedContent;
|
||||
|
||||
const getScanIteration = async (state) => {
|
||||
return await rsssl_api.runTest('mixed_content_scan', state).then((response) => {
|
||||
let data = response.data;
|
||||
if (typeof data === 'object') {
|
||||
data = Object.values(data);
|
||||
}
|
||||
if ( !Array.isArray(data) ) {
|
||||
data = [];
|
||||
}
|
||||
response.data = data;
|
||||
if ( state==='stop' ) {
|
||||
response.state = 'stop';
|
||||
}
|
||||
|
||||
return response;
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import {useState, useEffect} from "@wordpress/element";
|
||||
import {Button, ToggleControl} from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import ModalControl from "../../Modal/ModalControl";
|
||||
import Icon from "../../utils/Icon";
|
||||
import UseMixedContent from "./MixedContentData";
|
||||
import useModal from "../../Modal/ModalData";
|
||||
import React from "react";
|
||||
|
||||
const MixedContentScan = (props) => {
|
||||
const {fixedItems, ignoredItems} = useModal();
|
||||
const {fetchMixedContentData, mixedContentData, runScanIteration, start, stop, dataLoaded, action, scanStatus, progress, completedStatus, nonce, removeDataItem, ignoreDataItem} = UseMixedContent();
|
||||
const [showIgnoredUrls, setShowIgnoredUrls] = useState(false);
|
||||
const [resetPaginationToggle, setResetPaginationToggle] = useState(false);
|
||||
const [DataTable, setDataTable] = useState(null);
|
||||
const [theme, setTheme] = useState(null);
|
||||
useEffect( () => {
|
||||
import('react-data-table-component').then(({ default: DataTable, createTheme }) => {
|
||||
setDataTable(() => DataTable);
|
||||
setTheme(() => createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light'));
|
||||
});
|
||||
|
||||
}, []);
|
||||
useEffect( () => {
|
||||
fetchMixedContentData();
|
||||
}, [] );
|
||||
|
||||
useEffect( () => {
|
||||
if (scanStatus==='running') {
|
||||
runScanIteration()
|
||||
}
|
||||
}, [progress, scanStatus] );
|
||||
|
||||
const toggleIgnoredUrls = (e) => {
|
||||
setShowIgnoredUrls(!showIgnoredUrls);
|
||||
}
|
||||
|
||||
let field = props.field;
|
||||
let columns = [];
|
||||
field.columns.forEach(function(item, i) {
|
||||
let newItem = {
|
||||
name: item.name,
|
||||
sortable: item.sortable,
|
||||
grow: item.grow,
|
||||
selector: row => row[item.column],
|
||||
right: !!item.right,
|
||||
}
|
||||
columns.push(newItem);
|
||||
});
|
||||
|
||||
let dataTable = dataLoaded ? mixedContentData : [];
|
||||
|
||||
for (const item of dataTable) {
|
||||
item.warningControl = <span className="rsssl-task-status rsssl-warning">{__("Warning", "really-simple-ssl")}</span>
|
||||
|
||||
//check if an item was recently fixed or ignored, and update the table
|
||||
if (fixedItems.includes(item.id)) {
|
||||
item.fixed = true;
|
||||
}
|
||||
if (ignoredItems.includes(item.id)) {
|
||||
item.ignored = true;
|
||||
}
|
||||
//give fix and details the url as prop
|
||||
if ( item.fix ) {
|
||||
item.fix.url = item.blocked_url;
|
||||
item.fix.nonce = nonce;
|
||||
}
|
||||
if (item.details) {
|
||||
item.details.url = item.blocked_url;
|
||||
item.details.nonce = nonce;
|
||||
item.details.ignored = item.ignored;
|
||||
}
|
||||
if (item.location.length > 0) {
|
||||
if (item.location.indexOf('http://') !== -1 || item.location.indexOf('https://') !== -1) {
|
||||
item.locationControl =
|
||||
<a href={item.location} target="_blank" rel="noopener noreferrer">{__("View", "really-simple-ssl")}</a>
|
||||
} else {
|
||||
item.locationControl = item.location;
|
||||
}
|
||||
}
|
||||
item.detailsControl = item.details &&
|
||||
<ModalControl
|
||||
handleModal={props.handleModal}
|
||||
item={item}
|
||||
id={item.id}
|
||||
btnText={__("Details", "really-simple-ssl")}
|
||||
btnStyle={"secondary"}
|
||||
modalData={item.details}/>;
|
||||
item.fixControl = item.fix &&
|
||||
<ModalControl className={"button button-primary"}
|
||||
handleModal={props.handleModal}
|
||||
item={item}
|
||||
id={item.id}
|
||||
btnText={__("Fix", "really-simple-ssl")}
|
||||
btnStyle={"primary"}
|
||||
modalData={item.fix}/>;
|
||||
}
|
||||
|
||||
if ( !showIgnoredUrls ) {
|
||||
dataTable = dataTable.filter(
|
||||
item => !item.ignored,
|
||||
);
|
||||
}
|
||||
|
||||
//filter also recently fixed items
|
||||
dataTable = dataTable.filter(
|
||||
item => !item.fixed,
|
||||
);
|
||||
|
||||
let progressOutput =progress+'%';
|
||||
let startDisabled = scanStatus === 'running';
|
||||
let stopDisabled = scanStatus !== 'running';
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for head cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for data cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ExpandableRow = ({ data, disabled, handleFix }) => {
|
||||
return (
|
||||
<div className="rsssl-container">
|
||||
<div>
|
||||
<p>
|
||||
{data.details.description.map((item, i) => (
|
||||
<React.Fragment key={'fragment-'+i}>
|
||||
<span>{item}</span>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className=""
|
||||
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
{data.details.edit && (
|
||||
<a
|
||||
href={data.details.edit}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="button button-secondary"
|
||||
style={{display: 'flex', alignItems: 'center', justifyContent: 'center', marginRight: '10px' }}
|
||||
>
|
||||
{__("Edit", "really-simple-ssl")}
|
||||
</a>
|
||||
)}
|
||||
{data.details.help && (
|
||||
<button
|
||||
href={data.details.help}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="button button-red"
|
||||
style={{display: 'flex', alignItems: 'center', justifyContent: 'center', marginRight: '10px'}}
|
||||
>
|
||||
{__("Help", "really-simple-ssl")}
|
||||
</button>
|
||||
)}
|
||||
{!data.details.ignored && data.details.action === 'ignore_url' && (
|
||||
<button
|
||||
disabled={disabled}
|
||||
className="button button-primary"
|
||||
onClick={(e) => handleFix(e, 'ignore')}
|
||||
style={{display: 'flex', alignItems: 'center', justifyContent: 'center', marginRight: '10px'}}
|
||||
>
|
||||
{__("Ignore", "really-simple-ssl")}
|
||||
</button>
|
||||
)}
|
||||
{data.details.action !== 'ignore_url' && (
|
||||
<button
|
||||
disabled={disabled}
|
||||
className="button button-primary rsssl-action-buttons__button"
|
||||
onClick={(e) => handleFix(e, 'fix')}
|
||||
>
|
||||
{__("Fix", "really-simple-ssl")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-progress-container">
|
||||
<div className="rsssl-progress-bar" style={{width: progressOutput}} ></div>
|
||||
</div>
|
||||
{scanStatus==='running' && <div className="rsssl-current-scan-action">{action}</div>}
|
||||
{dataTable.length===0 && <>
|
||||
<div className="rsssl-mixed-content-description">
|
||||
{scanStatus!=='running' && completedStatus==='never' && __("No results. Start your first scan","really-simple-ssl")}
|
||||
{scanStatus!=='running' && completedStatus==='completed' && __("Everything is now served over SSL","really-simple-ssl")}
|
||||
</div>
|
||||
{ (scanStatus ==='running' || completedStatus!=='completed') && <div className="rsssl-mixed-content-placeholder">
|
||||
<div></div><div></div><div></div>
|
||||
</div>
|
||||
}
|
||||
{ scanStatus!=='running' && completedStatus==='completed' && <div className="rsssl-shield-overlay">
|
||||
<Icon name = "shield" size="80px"/>
|
||||
</div> }
|
||||
</>}
|
||||
{ DataTable && dataTable.length>0 &&
|
||||
<div className={'rsssl-mixed-content-datatable'}>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={dataTable}
|
||||
expandableRows
|
||||
expandableRowsComponent={ExpandableRow}
|
||||
dense
|
||||
pagination
|
||||
paginationResetDefaultPage={resetPaginationToggle} // optionally, a hook to reset pagination to page 1
|
||||
noDataComponent={__("No results", "really-simple-ssl")} //or your component
|
||||
theme={theme}
|
||||
customStyles={customStyles}
|
||||
/>
|
||||
</div> }
|
||||
<div className="rsssl-grid-item-content-footer">
|
||||
<button className="button" disabled={startDisabled} onClick={ () => start() }>{__("Start scan","really-simple-ssl")}</button>
|
||||
<button className="button" disabled={stopDisabled} onClick={ () => stop() }>{__("Stop","really-simple-ssl")}</button>
|
||||
<ToggleControl
|
||||
checked= { showIgnoredUrls==1 }
|
||||
onChange={ (e) => toggleIgnoredUrls(e) }
|
||||
/>
|
||||
<label>{__('Show ignored URLs', 'really-simple-ssl')}</label>
|
||||
</div>
|
||||
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default MixedContentScan;
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Notice after saving was successfull
|
||||
*/
|
||||
import { SnackbarList } from '@wordpress/components';
|
||||
import {
|
||||
useDispatch,
|
||||
useSelect,
|
||||
} from '@wordpress/data';
|
||||
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
|
||||
const Notices = () => {
|
||||
const notices = useSelect(
|
||||
( select ) =>
|
||||
select( noticesStore )
|
||||
.getNotices()
|
||||
.filter( ( notice ) => notice.type === 'snackbar' ),
|
||||
[]
|
||||
);
|
||||
if ( typeof notices === 'undefined' ) {
|
||||
return (<></>)
|
||||
}
|
||||
const { removeNotice } = useDispatch( noticesStore );
|
||||
return (
|
||||
<SnackbarList
|
||||
className="edit-site-notices"
|
||||
notices={ notices }
|
||||
onRemove={ removeNotice }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notices;
|
||||
@@ -0,0 +1,31 @@
|
||||
import useFields from "./FieldsData";
|
||||
|
||||
const Password = (props) => {
|
||||
const {updateField, setChangedField} = useFields();
|
||||
|
||||
const onChangeHandler = (fieldValue) => {
|
||||
updateField( props.field.id, fieldValue );
|
||||
setChangedField( props.field.id, fieldValue );
|
||||
}
|
||||
|
||||
/**
|
||||
* There is no "PasswordControl" in WordPress react yet, so we create our own license field.
|
||||
*/
|
||||
return (
|
||||
<div className="components-base-control">
|
||||
<div className="components-base-control__field">
|
||||
<label
|
||||
className="components-base-control__label"
|
||||
htmlFor={props.field.id}>{props.field.label}</label>
|
||||
<input className="components-text-control__input"
|
||||
type="password"
|
||||
id={props.field.id}
|
||||
value={props.field.value}
|
||||
onChange={ ( e ) => onChangeHandler(e.target.value) }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Password;
|
||||
@@ -0,0 +1,214 @@
|
||||
import {
|
||||
Button,
|
||||
SelectControl,
|
||||
} from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {useState,useEffect} from '@wordpress/element';
|
||||
import Icon from "../utils/Icon";
|
||||
import useFields from "./FieldsData";
|
||||
import React from "react";
|
||||
|
||||
const PermissionsPolicy = (props) => {
|
||||
const {fields, updateField, updateSubField, setChangedField, saveFields} = useFields();
|
||||
const [enablePermissionsPolicy, setEnablePermissionsPolicy] = useState(0);
|
||||
const [DataTable, setDataTable] = useState(null);
|
||||
const [theme, setTheme] = useState(null);
|
||||
const [rowsSelected, setRowsSelected] = useState([]);
|
||||
const [rowCleared, setRowCleared] = useState(false);
|
||||
useEffect( () => {
|
||||
import('react-data-table-component').then(({ default: DataTable, createTheme }) => {
|
||||
setDataTable(() => DataTable);
|
||||
setTheme(() => createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light'));
|
||||
});
|
||||
|
||||
}, []);
|
||||
|
||||
useEffect( () => {
|
||||
let field = fields.filter(field => field.id === 'enable_permissions_policy')[0];
|
||||
setEnablePermissionsPolicy(field.value);
|
||||
}, [] );
|
||||
|
||||
const onChangeHandler = (value, clickedItem ) => {
|
||||
let field= props.field;
|
||||
if (typeof field.value === 'object') {
|
||||
updateField(field.id, Object.values(field.value))
|
||||
}
|
||||
|
||||
//the updateItemId allows us to update one specific item in a field set.
|
||||
updateSubField(field.id, clickedItem.id, value);
|
||||
setChangedField(field.id, value);
|
||||
saveFields(true, false);
|
||||
}
|
||||
|
||||
const OnClickHandler = (selectedRows, value) => {
|
||||
let field= props.field;
|
||||
if (typeof field.value === 'object') {
|
||||
updateField(field.id, Object.values(field.value))
|
||||
}
|
||||
|
||||
selectedRows.forEach(row => {
|
||||
//the updateItemId allows us to update one specific item in a field set.
|
||||
updateSubField(field.id, row.id, value);
|
||||
setChangedField(field.id, value);
|
||||
});
|
||||
saveFields(true, false);
|
||||
|
||||
setRowCleared(true);
|
||||
setRowsSelected([]);
|
||||
// Reset rowCleared back to false after the DataTable has re-rendered
|
||||
setTimeout(() => setRowCleared(false), 0);
|
||||
}
|
||||
|
||||
|
||||
const togglePermissionsPolicyStatus = (e, enforce) => {
|
||||
e.preventDefault();
|
||||
//look up permissions policy enable field //enable_permissions_policy
|
||||
let field = fields.filter(field => field.id === 'enable_permissions_policy')[0];
|
||||
//enforce setting
|
||||
setEnablePermissionsPolicy(enforce);
|
||||
updateField(field.id, enforce);
|
||||
setChangedField(field.id, field.value);
|
||||
saveFields(true, false);
|
||||
}
|
||||
|
||||
let field = props.field;
|
||||
let fieldValue = field.value;
|
||||
const buttons = [
|
||||
'button-secondary',
|
||||
'button-primary',
|
||||
'button-red',
|
||||
];
|
||||
//we add a button property to the options
|
||||
|
||||
let options = props.options.map((option, index) => {
|
||||
option.button = buttons[index];
|
||||
return option;
|
||||
});
|
||||
|
||||
columns = [];
|
||||
field.columns.forEach(function(item, i) {
|
||||
let newItem = {
|
||||
name: item.name,
|
||||
sortable: item.sortable,
|
||||
width: item.width,
|
||||
selector: row => row[item.column],
|
||||
}
|
||||
columns.push(newItem);
|
||||
});
|
||||
let data = field.value;
|
||||
if (typeof data === 'object') {
|
||||
data = Object.values(data);
|
||||
}
|
||||
if (!Array.isArray(data) ) {
|
||||
data = [];
|
||||
}
|
||||
let disabled = false;
|
||||
let outputData = [];
|
||||
for (const item of data){
|
||||
let itemCopy = {...item};
|
||||
itemCopy.valueControl = <SelectControl
|
||||
help=''
|
||||
value={item.value}
|
||||
disabled={disabled}
|
||||
options={options}
|
||||
label=''
|
||||
onChange={ ( fieldValue ) => onChangeHandler( fieldValue, item, 'value' ) }
|
||||
/>
|
||||
outputData.push(itemCopy);
|
||||
}
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for head cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for data cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function handleSelection(state) {
|
||||
setRowCleared(false);
|
||||
setRowsSelected(state.selectedRows);
|
||||
}
|
||||
|
||||
if (!DataTable || !theme) return null;
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
{rowsSelected.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={"rsssl-multiselect-datatable-form rsssl-primary"}
|
||||
>
|
||||
<div>
|
||||
{__("You have selected", "really-simple-ssl")} {rowsSelected.length} {__("rows", "really-simple-ssl")}
|
||||
</div>
|
||||
|
||||
<div className="rsssl-action-buttons">
|
||||
{options.map((option) => (
|
||||
<div className="rsssl-action-buttons__inner" key={'option-'+option.value}>
|
||||
<Button
|
||||
// className={"button button-red rsssl-action-buttons__button"}
|
||||
className={"button " + option.button + " rsssl-action-buttons__button"}
|
||||
onClick={ ( fieldValue ) => OnClickHandler( rowsSelected, option.value ) }
|
||||
>
|
||||
{option.value === 'self' ? __("Reset", "really-simple-ssl") : __(option.label, "really-simple-ssl")}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={outputData}
|
||||
dense
|
||||
pagination={false}
|
||||
customStyles={customStyles}
|
||||
theme={theme}
|
||||
selectableRows
|
||||
selectableRowsHighlight={true}
|
||||
onSelectedRowsChange={handleSelection}
|
||||
clearSelectedRows={rowCleared}
|
||||
/>
|
||||
{ enablePermissionsPolicy!=1 && <button className="button button-primary" onClick={ (e) => togglePermissionsPolicyStatus(e, true ) }>{__("Enforce","really-simple-ssl")}</button> }
|
||||
{ enablePermissionsPolicy==1 && <div className="rsssl-locked">
|
||||
<div className="rsssl-shield-overlay">
|
||||
<Icon name = "shield" size="80px"/>
|
||||
</div>
|
||||
<div className="rsssl-locked-overlay">
|
||||
<span className="rsssl-progress-status rsssl-learning-mode-enforced">{__("Enforced","really-simple-ssl")}</span>
|
||||
{ props.disabled && <>{ __("Permissions Policy is set outside Really Simple Security.", "really-simple-ssl")} </>}
|
||||
{ !props.disabled && <>{__("Permissions Policy is enforced.", "really-simple-ssl")} </>}
|
||||
{ !props.disabled && <a className="rsssl-learning-mode-link" href="#" onClick={ (e) => togglePermissionsPolicyStatus(e, false) }>{__("Disable", "really-simple-ssl") }</a> }
|
||||
</div>
|
||||
</div>}
|
||||
{ props.disabled && enablePermissionsPolicy!=1 && <div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay">
|
||||
<span className="rsssl-progress-status rsssl-disabled">{__("Disabled","really-simple-ssl")}</span>
|
||||
{__("The Permissions Policy has been disabled.", "really-simple-ssl")}
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default PermissionsPolicy
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* This file contains the PostDropdown component.
|
||||
*
|
||||
* This component displays a dropdown menu that allows the user to select a post
|
||||
* from a list of posts fetched from the WordPress database. The selected post
|
||||
* is then used to set a value in an options array stored in the WordPress
|
||||
* database. The component also allows the user to search for posts by typing
|
||||
* in a search box.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import autoCompleteSharedTheme from '../utils/autoCompleteTheme';
|
||||
import AutoCompleteControl from "../settings/AutoComplete/AutoCompleteControl";
|
||||
import useFields from "./FieldsData";
|
||||
|
||||
const PostDropdown = ({ field }) => {
|
||||
const [posts, setPosts] = useState([]);
|
||||
const [selectedPost, setSelectedPost] = useState("");
|
||||
const { updateField, setChangedField } = useFields();
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch posts data when the component mounts
|
||||
apiFetch({ path: '/wp/v2/pages?per_page=100' }).then((data) => {
|
||||
const formattedData = data.map(post => ({
|
||||
label: post.title.rendered,
|
||||
value: post.id
|
||||
}));
|
||||
setPosts([{ label: "404 (default)", value: "404_default" }, ...formattedData]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (field.value !== "404_default") {
|
||||
apiFetch({ path: `wp/v2/pages/${field.value}` })
|
||||
.then((data) => {
|
||||
if (data.title) {
|
||||
setSelectedPost({ label: data.title.rendered, value: field.value });
|
||||
} else {
|
||||
setSelectedPost({ label: "404 (default)", value: "404_default" });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setSelectedPost({ label: "404 (default)", value: "404_default" });
|
||||
}
|
||||
}, [field.value]);
|
||||
|
||||
const handleChange = (newValue) => {
|
||||
const value = newValue ? newValue : '404_default';
|
||||
updateField(field.id, value);
|
||||
setChangedField(field.id, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={autoCompleteSharedTheme}>
|
||||
<div>
|
||||
<label htmlFor="rsssl-filter-post-input">
|
||||
{__("Redirect to this post when someone tries to access /wp-admin or /wp-login.php. The default is a 404 page.", "really-simple-ssl")}
|
||||
</label>
|
||||
<AutoCompleteControl
|
||||
className="rsssl-select"
|
||||
field={field}
|
||||
label={__("Search for a post.", "really-simple-ssl")}
|
||||
value={selectedPost}
|
||||
options={posts}
|
||||
onChange={handleChange}
|
||||
disabled={false}
|
||||
/>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostDropdown;
|
||||
@@ -0,0 +1,47 @@
|
||||
import {__} from "@wordpress/i18n";
|
||||
import useLicense from "./License/LicenseData";
|
||||
import Hyperlink from "../utils/Hyperlink";
|
||||
|
||||
const PremiumOverlay = ({msg, title, upgrade}) => {
|
||||
const {licenseStatus} = useLicense();
|
||||
let pro_plugin_active = rsssl_settings.pro_plugin_active === '1'
|
||||
let target = pro_plugin_active ? '_self' : '_blank';
|
||||
let upgradeButtonText = pro_plugin_active ? __("Check license", "really-simple-ssl") : __("Go Pro", "really-simple-ssl");
|
||||
let upgradeUrl = upgrade ? upgrade : rsssl_settings.upgrade_link;
|
||||
if (pro_plugin_active) {
|
||||
upgradeUrl = '#settings/license';
|
||||
}
|
||||
let message = msg ? msg : <Hyperlink text={__("Learn more about %sPremium%s", "really-simple-ssl")} url={upgradeUrl}/>;
|
||||
if ( pro_plugin_active ) {
|
||||
if (licenseStatus === 'empty' || licenseStatus === 'deactivated') {
|
||||
message = rsssl_settings.messageInactive;
|
||||
} else {
|
||||
message = rsssl_settings.messageInvalid;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rsssl-locked rsssl-locked-premium">
|
||||
<div className="rsssl-locked-overlay rsssl-premium">
|
||||
{/* header */}
|
||||
<div className="rsssl-locked-header">
|
||||
<h5 className={'rsssl-locked-header-title'}>{title}</h5>
|
||||
</div>
|
||||
<div className="rsssl-locked-content">
|
||||
<span>{message} </span>
|
||||
</div>
|
||||
<div className="rsssl-locked-footer">
|
||||
{/* We place a button on the left side */}
|
||||
<div className="rsssl-grid-item-footer-buttons">
|
||||
<a
|
||||
className="button button-primary left"
|
||||
href={upgradeUrl} target={target}>{upgradeButtonText}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PremiumOverlay;
|
||||
@@ -0,0 +1,122 @@
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import useFields from "../FieldsData";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import {useEffect, useState, useRef} from '@wordpress/element';
|
||||
import useRiskData from "./RiskData";
|
||||
import hoverTooltip from "../../utils/hoverTooltip";
|
||||
|
||||
const NotificationTester = (props) => {
|
||||
const {
|
||||
fetchVulnerabilities,riskLevels
|
||||
} = useRiskData();
|
||||
const [disabled, setDisabled] = useState(true);
|
||||
const [mailNotificationsEnabled, setMailNotificationsEnabled] = useState(true);
|
||||
const [vulnerabilitiesEnabled, setVulnerabilitiesEnabled] = useState(false);
|
||||
const [vulnerabilitiesSaved, setVulnerabilitiesSaved] = useState(false);
|
||||
const {addHelpNotice, fields, getFieldValue, updateField, setChangedField, fieldAlreadyEnabled, fetchFieldsData, updateFieldAttribute} = useFields();
|
||||
|
||||
let disabledButtonPropBoolean = props.disabled;
|
||||
let disabledButtonViaFieldConfig = props.field.disabled;
|
||||
let disabledButton = (disabledButtonViaFieldConfig || disabledButtonPropBoolean);
|
||||
|
||||
const buttonRef = useRef(null);
|
||||
|
||||
let tooltipText = '';
|
||||
let emptyValues = [undefined, null, ''];
|
||||
|
||||
if (disabled
|
||||
&& props.field.hasOwnProperty('disabledTooltipText')
|
||||
&& !emptyValues.includes(props.field.disabledTooltipHoverText)
|
||||
) {
|
||||
tooltipText = props.field.disabledTooltipHoverText;
|
||||
}
|
||||
|
||||
hoverTooltip(
|
||||
buttonRef,
|
||||
(disabledButton && (tooltipText !== '')),
|
||||
tooltipText
|
||||
);
|
||||
|
||||
useEffect ( () => {
|
||||
let mailEnabled = getFieldValue('send_notifications_email') == 1;
|
||||
let mailVerified = rsssl_settings.email_verified;
|
||||
let vulnerabilities = fieldAlreadyEnabled('enable_vulnerability_scanner');
|
||||
setMailNotificationsEnabled(mailEnabled);
|
||||
let enableButton = mailVerified && vulnerabilities;
|
||||
setDisabled(! enableButton);
|
||||
setMailNotificationsEnabled(mailEnabled);
|
||||
setVulnerabilitiesSaved(vulnerabilities);
|
||||
setVulnerabilitiesEnabled(getFieldValue('enable_vulnerability_scanner') == 1)
|
||||
},[fields])
|
||||
|
||||
const doTestNotification = async () => {
|
||||
//Test the notifications
|
||||
setDisabled(true);
|
||||
rsssl_api.doAction( 'vulnerabilities_test_notification' ).then( () => {
|
||||
setDisabled(false);
|
||||
fetchFieldsData('vulnerabilities');
|
||||
fetchVulnerabilities();
|
||||
addHelpNotice(
|
||||
props.field.id,
|
||||
'success',
|
||||
__('All notifications are triggered successfully, please check your email to double-check if you can receive emails.','really-simple-ssl'),
|
||||
__('Test notifications','really-simple-ssl'),
|
||||
false
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
//ensure that risk levels are enabled cascading
|
||||
useEffect( () => {
|
||||
let dashboardRiskLevel = getFieldValue('vulnerability_notification_dashboard');
|
||||
dashboardRiskLevel = riskLevels.hasOwnProperty(dashboardRiskLevel) ? riskLevels[dashboardRiskLevel] : 0;
|
||||
// let siteWideRiskLevel = getFieldValue('vulnerability_notification_sitewide');
|
||||
//the sitewide risk level should be at least as high as the dashboard risk level. Disable lower risk levels in sitewide
|
||||
//create an array of ints from 1 to dashboardRiskLevel, we drop the * from the array
|
||||
let priorDashboardRiskLevel = dashboardRiskLevel>0 ? dashboardRiskLevel-1 :dashboardRiskLevel;
|
||||
let dashboardRiskLevels = Array.from(Array(priorDashboardRiskLevel).keys()).map(x => x );
|
||||
//convert these integers back to risk levels
|
||||
//find the integer value in the riskLevels object, and return the key
|
||||
dashboardRiskLevels = dashboardRiskLevels.map( (level) => {
|
||||
return Object.keys(riskLevels).find(key => riskLevels[key] === level );
|
||||
});
|
||||
|
||||
if (dashboardRiskLevels.length > 0) {
|
||||
updateFieldAttribute('vulnerability_notification_sitewide', 'disabled', dashboardRiskLevels);
|
||||
//if the current value is below the dashboardRisk Level, set it to the dashboardRiskLevel
|
||||
let siteWideRiskLevel = getFieldValue('vulnerability_notification_sitewide');
|
||||
siteWideRiskLevel = riskLevels.hasOwnProperty(siteWideRiskLevel) ? riskLevels[siteWideRiskLevel] : 0;
|
||||
if (siteWideRiskLevel<dashboardRiskLevel) {
|
||||
let newRiskLevel = Object.keys(riskLevels).find(key => riskLevels[key] === dashboardRiskLevel );
|
||||
updateField('vulnerability_notification_sitewide', newRiskLevel);
|
||||
setChangedField('vulnerability_notification_sitewide', newRiskLevel);
|
||||
}
|
||||
} else {
|
||||
updateFieldAttribute('vulnerability_notification_sitewide', 'disabled', false);
|
||||
}
|
||||
},[getFieldValue('vulnerability_notification_dashboard')])
|
||||
|
||||
let fieldCopy = {...props.field};
|
||||
if (!mailNotificationsEnabled) {
|
||||
fieldCopy.tooltip = __('You have not enabled the email notifications in the general settings.','really-simple-ssl');
|
||||
fieldCopy.warning = true;
|
||||
} else if (vulnerabilitiesEnabled && !vulnerabilitiesSaved) {
|
||||
fieldCopy.tooltip = __('The notification test only works if you save the setting first.','really-simple-ssl');
|
||||
fieldCopy.warning = true;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<label>{props.labelWrap(fieldCopy)}</label>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={() => doTestNotification()}
|
||||
disabled={disabled}
|
||||
className="button button-default"
|
||||
>
|
||||
{props.field.button_text}
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotificationTester
|
||||
@@ -0,0 +1,107 @@
|
||||
import {useEffect,useState} from '@wordpress/element';
|
||||
import UseRiskData from "./RiskData";
|
||||
import useFields from "../FieldsData";
|
||||
import {__} from "@wordpress/i18n";
|
||||
|
||||
const RiskComponent = (props) => {
|
||||
//first we put the data in a state
|
||||
const {riskData, dummyRiskData, processing, dataLoaded, fetchVulnerabilities, updateRiskData} = UseRiskData();
|
||||
const { fields, fieldAlreadyEnabled, getFieldValue, setChangedField, updateField, saveFields} = useFields();
|
||||
const [measuresEnabled, setMeasuresEnabled] = useState(false);
|
||||
const [vulnerabilityDetectionEnabled, setVulnerabilityDetectionEnabled] = useState(false);
|
||||
const [DataTable, setDataTable] = useState(null);
|
||||
const [theme, setTheme] = useState(null);
|
||||
useEffect( () => {
|
||||
import('react-data-table-component').then(({ default: DataTable, createTheme }) => {
|
||||
setDataTable(() => DataTable);
|
||||
setTheme(() => createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light'));
|
||||
});
|
||||
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if ( fieldAlreadyEnabled('enable_vulnerability_scanner')) {
|
||||
if (!dataLoaded) {
|
||||
fetchVulnerabilities();
|
||||
}
|
||||
}
|
||||
let vulnerabilitiesEnabled = fieldAlreadyEnabled('enable_vulnerability_scanner' );
|
||||
setVulnerabilityDetectionEnabled(vulnerabilitiesEnabled);
|
||||
let measuresOn = getFieldValue('measures_enabled')==1;
|
||||
setMeasuresEnabled(measuresOn);
|
||||
}, [fields]);
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*/
|
||||
|
||||
useEffect(() => {
|
||||
let enabled = getFieldValue('measures_enabled')==1;
|
||||
setMeasuresEnabled(enabled);
|
||||
}, [] );
|
||||
|
||||
//we create the columns
|
||||
let columns = [];
|
||||
//getting the fields from the props
|
||||
let field = props.field;
|
||||
//we loop through the fields
|
||||
field.columns.forEach(function (item, i) {
|
||||
let newItem = buildColumn(item)
|
||||
columns.push(newItem);
|
||||
});
|
||||
|
||||
//now we get the options for the select control
|
||||
let options = props.field.options;
|
||||
//we divide the key into label and the value into value
|
||||
options = Object.entries(options).map((item) => {
|
||||
return {label: item[1], value: item[0]};
|
||||
});
|
||||
|
||||
//and we add the select control to the data
|
||||
let data = Array.isArray(riskData) ? [...riskData] : [];
|
||||
data = data.length===0 ? [...dummyRiskData] : data;
|
||||
let disabled = !vulnerabilityDetectionEnabled || !measuresEnabled;
|
||||
for (const key in data) {
|
||||
let dataItem = {...data[key]}
|
||||
dataItem.riskSelection = <select disabled={processing || disabled} value={dataItem.value} onChange={(e) => onChangeHandler(e.target.value, dataItem)}>
|
||||
{options.map((option,i) => <option key={'risk-'+i} value={option.value} disabled={ dataItem.disabledRiskLevels && dataItem.disabledRiskLevels.includes(option.value)} >{option.label}</option>) }
|
||||
</select>
|
||||
data[key] = dataItem;
|
||||
}
|
||||
let processingClass = disabled ? 'rsssl-processing' : '';
|
||||
|
||||
return (
|
||||
<div>
|
||||
{DataTable && <DataTable
|
||||
columns={columns}
|
||||
data={Object.values(data)}
|
||||
dense
|
||||
pagination={false}
|
||||
persistTableHead
|
||||
noDataComponent={__("No vulnerabilities found", "really-simple-ssl")}
|
||||
theme={theme}
|
||||
/> }
|
||||
</div>
|
||||
)
|
||||
|
||||
function buildColumn(column) {
|
||||
return {
|
||||
name: column.name,
|
||||
sortable: column.sortable,
|
||||
width: column.width,
|
||||
selector: row => row[column.column],
|
||||
grow: column.grow,
|
||||
};
|
||||
}
|
||||
|
||||
function onChangeHandler(fieldValue, item) {
|
||||
updateRiskData(item.id, fieldValue);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default RiskComponent;
|
||||
@@ -0,0 +1,201 @@
|
||||
/* Creates A Store For Risk Data using Zustand */
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import {produce} from "immer";
|
||||
import React from "react";
|
||||
import {addUrlRef} from "../../utils/AddUrlRef";
|
||||
|
||||
const UseRiskData = create((set, get) => ({
|
||||
|
||||
dummyRiskData: [
|
||||
{id:'force_update',name:'Force Update',value:'l',description:__('Force update the plugin or theme','really-simple-ssl')},
|
||||
{id:'quarantine',name:'Quarantine',value:'m',description:__('Isolates the plugin or theme if no update can be performed','really-simple-ssl')},
|
||||
],
|
||||
riskData:[],
|
||||
riskLevels: {
|
||||
l: 1,
|
||||
m: 2,
|
||||
h: 3,
|
||||
c: 4,
|
||||
},
|
||||
vulnerabilities: [],
|
||||
processing:false,
|
||||
dataLoaded: false,
|
||||
// Stuff we need for the WPVulData component
|
||||
updates: 0, //for letting the component know if there are updates available
|
||||
HighestRisk: false, //for storing the highest risk
|
||||
lastChecked: '', //for storing the last time the data was checked
|
||||
vulEnabled: false, //for storing the status of the vulnerability scan
|
||||
riskNaming: {}, //for storing the risk naming
|
||||
vulList: [], //for storing the list of vulnerabilities
|
||||
setDataLoaded: (value) => set({dataLoaded: value}),
|
||||
//update Risk Data
|
||||
updateRiskData: async (field, value) => {
|
||||
if (get().processing) return;
|
||||
set({processing:true});
|
||||
|
||||
set(
|
||||
produce((state) => {
|
||||
let index = state.riskData.findIndex((item) => item.id === field);
|
||||
state.riskData[index].value = value;
|
||||
state.riskData = get().enforceCascadingRiskLevels(state.riskData);
|
||||
})
|
||||
);
|
||||
try {
|
||||
await rsssl_api.doAction('vulnerabilities_measures_set', {
|
||||
riskData: get().riskData,
|
||||
});
|
||||
|
||||
set({dataLoaded: true, processing:false});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
set({processing:false})
|
||||
},
|
||||
enforceCascadingRiskLevels: (data) => {
|
||||
if (data.length===0) return data;
|
||||
//get risk levels for force_update
|
||||
let forceUpdateRiskLevel = data.filter((item) => item.id==='force_update')[0].value;
|
||||
let quarantineRiskLevel = data.filter((item) => item.id==='quarantine')[0].value;
|
||||
|
||||
//get the integer value of the risk level
|
||||
forceUpdateRiskLevel = get().riskLevels.hasOwnProperty(forceUpdateRiskLevel) ? get().riskLevels[forceUpdateRiskLevel] : 5;
|
||||
quarantineRiskLevel = get().riskLevels.hasOwnProperty(quarantineRiskLevel) ? get().riskLevels[quarantineRiskLevel] : 5;
|
||||
let quarantineIndex = data.findIndex((item) => item.id==='quarantine');
|
||||
//if the quarantine risk level is lower than the force update risk level, we set it to the force update risk level
|
||||
if (quarantineRiskLevel<forceUpdateRiskLevel) {
|
||||
data[quarantineIndex].value = Object.keys(get().riskLevels).find(key => get().riskLevels[key] === forceUpdateRiskLevel);
|
||||
}
|
||||
//if the force update risk level is none, set quarantine also to none.
|
||||
if ( forceUpdateRiskLevel===5 ) {
|
||||
data[quarantineIndex].value = '*';
|
||||
}
|
||||
|
||||
//disable all values below this value
|
||||
let disableUpTo = forceUpdateRiskLevel>0 ? forceUpdateRiskLevel : 0
|
||||
//create an array of integers up to the forceUpdateRiskLevel
|
||||
let disabledRiskLevels = Array.from(Array(disableUpTo).keys()).map(x => x);
|
||||
disabledRiskLevels = disabledRiskLevels.map( (level) => {
|
||||
return Object.keys(get().riskLevels).find(key => get().riskLevels[key] === level );
|
||||
});
|
||||
data[quarantineIndex].disabledRiskLevels = disabledRiskLevels;
|
||||
return data;
|
||||
},
|
||||
fetchFirstRun: async () => {
|
||||
if (get().processing) return;
|
||||
set({processing:true});
|
||||
await rsssl_api.doAction('vulnerabilities_scan_files');
|
||||
set({processing:false});
|
||||
},
|
||||
|
||||
/*
|
||||
* Functions
|
||||
*/
|
||||
fetchVulnerabilities: async () => {
|
||||
if (get().processing) return;
|
||||
set({processing:true});
|
||||
let data = {};
|
||||
try {
|
||||
const fetched = await rsssl_api.doAction('hardening_data', data);
|
||||
let vulList = [];
|
||||
let vulnerabilities = 0;
|
||||
if (fetched.data.vulList) {
|
||||
vulnerabilities = fetched.data.vulnerabilities;
|
||||
vulList = fetched.data.vulList;
|
||||
if (typeof vulList === 'object') {
|
||||
//we make it an array
|
||||
vulList = Object.values(vulList);
|
||||
}
|
||||
vulList.forEach(function (item, i) {
|
||||
let updateUrl = item.update_available ? rsssl_settings.plugins_url + "?plugin_status=upgrade" : '#settings/vulnerabilities';
|
||||
item.vulnerability_action = <div className="rsssl-action-buttons">
|
||||
<a className="rsssl-button button-secondary"
|
||||
href={addUrlRef("https://really-simple-ssl.com/vulnerability/" + item.rss_identifier)}
|
||||
target={"_blank"} rel="noopener noreferrer">{__("Details", "really-simple-ssl")}</a>
|
||||
<a disabled={!item.update_available} href={updateUrl}
|
||||
className="rsssl-button button-primary"
|
||||
>{__("Update", "really-simple-ssl")}</a>
|
||||
</div>
|
||||
});
|
||||
}
|
||||
let riskData = fetched.data.riskData;
|
||||
if (!Array.isArray(riskData)) {riskData = []}
|
||||
riskData = get().enforceCascadingRiskLevels(riskData);
|
||||
set(
|
||||
produce((state) => {
|
||||
state.vulnerabilities = vulnerabilities;
|
||||
state.vulList = vulList;
|
||||
state.updates = fetched.data.updates;
|
||||
state.dataLoaded = true;
|
||||
state.riskNaming = fetched.data.riskNaming;
|
||||
state.lastChecked = fetched.data.lastChecked;
|
||||
state.vulEnabled = fetched.data.vulEnabled;
|
||||
state.riskData = riskData;
|
||||
state.processing = false;
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
|
||||
vulnerabilityCount: () => {
|
||||
let vuls = get().vulList;
|
||||
//we group the data by risk level
|
||||
//first we make vuls an array
|
||||
let vulsArray = [];
|
||||
Object.keys(vuls).forEach(function (key) {
|
||||
vulsArray.push(vuls[key]);
|
||||
});
|
||||
let riskLevels = ['c', 'h', 'm', 'l'];
|
||||
//we count the amount of vulnerabilities per risk level
|
||||
return riskLevels.map(function (level) {
|
||||
return {
|
||||
level: level,
|
||||
count: vulsArray.filter(function (vul) {
|
||||
return vul.risk_level === level;
|
||||
}).length
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
vulnerabilityScore: () => {
|
||||
let score = 0;
|
||||
let vulnerabilitiesList = get().vulList;
|
||||
|
||||
Object.keys(vulnerabilitiesList).forEach(function (key) {
|
||||
//if there are vulnerabilities with critical severity, score is 5
|
||||
if (vulnerabilitiesList[key].risk_level === 'c') {
|
||||
score = 5;
|
||||
} else if (score < 1) {
|
||||
score = 1;
|
||||
}
|
||||
});
|
||||
return score;
|
||||
},
|
||||
|
||||
hardeningScore: () => {
|
||||
let score = 0;
|
||||
let vulnerabilitiesList = get().vulnerabilities;
|
||||
for (let i = 0; i < vulnerabilitiesList.length; i++) {
|
||||
score += vulnerabilitiesList[i].hardening_score;
|
||||
}
|
||||
return score;
|
||||
},
|
||||
|
||||
activateVulnerabilityScanner: async () => {
|
||||
try {
|
||||
const fetched = await rsssl_api.doAction('rsssl_scan_files');
|
||||
if (fetched.request_success) {
|
||||
//we get the data again
|
||||
await get().fetchVulnerabilities();
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
export default UseRiskData;
|
||||
@@ -0,0 +1,58 @@
|
||||
import {create} from "zustand";
|
||||
import {produce} from "immer";
|
||||
import {__} from "@wordpress/i18n";
|
||||
|
||||
const useRunnerData = create((set, get) => ({
|
||||
showIntro:false,
|
||||
setShowIntro: (value) => set({showIntro: value}),
|
||||
disabled:true,
|
||||
introCompleted: false, //for storing the status of the first run
|
||||
setIntroCompleted: (value) => {
|
||||
set({introCompleted: value});
|
||||
},
|
||||
setDisabled(disabled) {
|
||||
set(state => ({disabled}))
|
||||
},
|
||||
list:[
|
||||
{
|
||||
'id':'initialize',
|
||||
'icon':'loading',
|
||||
'color':'black',
|
||||
'text': __("Preparing vulnerability detection", "really-simple-ssl"),
|
||||
},
|
||||
{
|
||||
'id':'fetchVulnerabilities',
|
||||
'icon':'loading',
|
||||
'color':'black',
|
||||
'text': __("Collecting plugin, theme and core data", "really-simple-ssl"),
|
||||
},
|
||||
{
|
||||
'id':'scan',
|
||||
'icon':'loading',
|
||||
'color':'black',
|
||||
'text': __("Scanning your WordPress configuration", "really-simple-ssl"),
|
||||
},
|
||||
{
|
||||
'id':'enabled',
|
||||
'icon':'loading',
|
||||
'color':'black',
|
||||
'text': __("Reporting enabled", "really-simple-ssl"),
|
||||
},
|
||||
],
|
||||
setItemCompleted: async (id) => {
|
||||
const stepIndex = get().list.findIndex(item => {
|
||||
return item.id===id;
|
||||
});
|
||||
set(
|
||||
produce((state) => {
|
||||
const item = state.list[stepIndex];
|
||||
item.icon = 'circle-check';
|
||||
item.color = 'green';
|
||||
state.list[stepIndex] = item;
|
||||
})
|
||||
)
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
export default useRunnerData;
|
||||
@@ -0,0 +1,176 @@
|
||||
import {__} from '@wordpress/i18n';
|
||||
import useRiskData from "./RiskData";
|
||||
import {useEffect, useState} from '@wordpress/element';
|
||||
import DataTable, {createTheme} from "react-data-table-component";
|
||||
import useFields from "../FieldsData";
|
||||
import useProgress from "../../Dashboard/Progress/ProgressData";
|
||||
import useRunnerData from "./RunnerData";
|
||||
import './datatable.scss';
|
||||
|
||||
const VulnerabilitiesOverview = (props) => {
|
||||
const {getProgressData} = useProgress();
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
||||
const {
|
||||
dataLoaded,
|
||||
vulList,
|
||||
fetchVulnerabilities,
|
||||
setDataLoaded,
|
||||
fetchFirstRun
|
||||
} = useRiskData();
|
||||
const {getFieldValue, handleNextButtonDisabled, fieldAlreadyEnabled, fieldsLoaded} = useFields();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
//we create the columns
|
||||
let columns = [];
|
||||
//getting the fields from the props
|
||||
let field = props.field;
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for head cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for data cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light');
|
||||
|
||||
function buildColumn(column) {
|
||||
return {
|
||||
name: column.name,
|
||||
sortable: column.sortable,
|
||||
visible: column.visible,
|
||||
selector: row => row[column.column],
|
||||
searchable: column.searchable,
|
||||
grow:column.grow,
|
||||
width: column.width,
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!fieldsLoaded) return;
|
||||
setEnabled(getFieldValue('enable_vulnerability_scanner') == 1);
|
||||
}, [getFieldValue('enable_vulnerability_scanner')])
|
||||
|
||||
let dummyData = [['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', '']];
|
||||
field.columns.forEach(function (item, i) {
|
||||
let newItem = buildColumn(item)
|
||||
columns.push(newItem);
|
||||
});
|
||||
|
||||
//get data if field was already enabled, so not changed right now.
|
||||
useEffect(() => {
|
||||
let vulnerabilityDetectionEnabledAndSaved = fieldAlreadyEnabled('enable_vulnerability_scanner');
|
||||
|
||||
// let introShown = getFieldValue('vulnerabilities_intro_shown') == 1;
|
||||
if ( !vulnerabilityDetectionEnabledAndSaved ) {
|
||||
return;
|
||||
}
|
||||
setDataLoaded(false);
|
||||
|
||||
}, [ getFieldValue('enable_vulnerability_scanner') ]);
|
||||
|
||||
useEffect(() => {
|
||||
if ( dataLoaded ) {
|
||||
return;
|
||||
}
|
||||
|
||||
let vulnerabilityDetectionEnabledAndSaved = fieldAlreadyEnabled('enable_vulnerability_scanner');
|
||||
if ( vulnerabilityDetectionEnabledAndSaved ) {
|
||||
//if just enabled, but intro already shown, just get the first run data.
|
||||
initialize();
|
||||
}
|
||||
|
||||
}, [ dataLoaded ]);
|
||||
|
||||
const initialize = async () => {
|
||||
await fetchFirstRun();
|
||||
await fetchVulnerabilities();
|
||||
await getProgressData();
|
||||
}
|
||||
|
||||
let data = vulList.map(item => ({
|
||||
...item,
|
||||
risk_name: <span className={"rsssl-badge-large rsp-risk-level-" + item.risk_level}>
|
||||
{/* Convert the first character to uppercase and append the rest of the string */}
|
||||
{item.risk_name.charAt(0).toUpperCase() + item.risk_name.slice(1).replace('-risk', '')}
|
||||
</span>
|
||||
}));
|
||||
if (searchTerm.length > 0) {
|
||||
data = data.filter(function (item) {
|
||||
//we check if the search value is in the name or the risk name
|
||||
if (item.Name.toLowerCase().includes(searchTerm.toLowerCase())) {
|
||||
return item;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{marginTop: '5px'}}>
|
||||
{!enabled ? (
|
||||
<>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={dummyData}
|
||||
dense
|
||||
pagination
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
theme="really-simple-plugins"
|
||||
customStyles={customStyles}
|
||||
/>
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay">
|
||||
<span className="rsssl-task-status rsssl-open">
|
||||
{__('Disabled', 'really-simple-ssl')}
|
||||
</span>
|
||||
<span>
|
||||
{__('Activate vulnerability detection to enable this block.', 'really-simple-ssl')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="rsssl-container">
|
||||
<div className="rsssl-search-bar">
|
||||
<div className="rsssl-search-bar__inner">
|
||||
<div className="rsssl-search-bar__icon"></div>
|
||||
<input
|
||||
type="text"
|
||||
className="rsssl-search-bar__input"
|
||||
placeholder={__("Search", "really-simple-ssl")}
|
||||
onKeyUp={event => {
|
||||
setSearchTerm(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
dense
|
||||
pagination
|
||||
persistTableHead
|
||||
noDataComponent={__("No vulnerabilities found", "really-simple-ssl")}
|
||||
theme="really-simple-plugins"
|
||||
customStyles={customStyles}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default VulnerabilitiesOverview;
|
||||
@@ -0,0 +1,12 @@
|
||||
.rsssl-vulnerabilities_overview {
|
||||
div[data-column-id="4"].rdt_TableCol {
|
||||
display:block;
|
||||
}
|
||||
|
||||
.rdt_TableCell:last-child {
|
||||
flex: auto;
|
||||
padding-right: 20px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.rsssl-modal.rsssl-vulnerabilities-modal{
|
||||
ul {
|
||||
column-count: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import Select from 'react-select';
|
||||
import useFields from "./FieldsData";
|
||||
import useRolesData from './TwoFA/RolesStore';
|
||||
import {__} from "@wordpress/i18n";
|
||||
import './TwoFA/select.scss';
|
||||
/**
|
||||
* RolesDropDown component represents a dropdown select for excluding roles
|
||||
* from two-factor authentication email.
|
||||
* @param {object} field - The field object containing information about the field.
|
||||
*/
|
||||
const RolesDropDown = ({ field }) => {
|
||||
const {fetchRoles, roles, rolesLoaded} = useRolesData();
|
||||
const [selectedRoles, setSelectedRoles] = useState([]);
|
||||
const [rolesEnabled, setRolesEnabled] = useState(false);
|
||||
|
||||
// Custom hook to manage form fields
|
||||
const { updateField, setChangedField, fieldsLoaded,getFieldValue } = useFields();
|
||||
let enabled = true;
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!rolesLoaded) {
|
||||
fetchRoles(field.id);
|
||||
}
|
||||
}, [rolesLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if ( !field.value ) {
|
||||
setChangedField(field.id, field.default);
|
||||
updateField(field.id, field.default);
|
||||
setSelectedRoles( field.default.map((role, index) => ({ value: role, label: role.charAt(0).toUpperCase() + role.slice(1) })));
|
||||
}
|
||||
setSelectedRoles( field.value.map((role, index) => ({ value: role, label: role.charAt(0).toUpperCase() + role.slice(1) })));
|
||||
},[fieldsLoaded]);
|
||||
|
||||
|
||||
//if the field enforce_frequent_password_change is enabled, then the field is enabled
|
||||
useEffect(() => {
|
||||
setRolesEnabled(getFieldValue('enforce_frequent_password_change'));
|
||||
},[getFieldValue('enforce_frequent_password_change')]);
|
||||
|
||||
/**
|
||||
* Handles the change event of the react-select component.
|
||||
* @param {array} selectedOptions - The selected options from the dropdown.
|
||||
*/
|
||||
const handleChange = (selectedOptions) => {
|
||||
// Extract the values of the selected options
|
||||
const rolesExcluded = selectedOptions.map(option => option.value);
|
||||
// Update the selectedRoles state
|
||||
setSelectedRoles(selectedOptions);
|
||||
// Update the field and changedField using the custom hook functions
|
||||
updateField(field.id, rolesExcluded);
|
||||
setChangedField(field.id, rolesExcluded);
|
||||
};
|
||||
|
||||
const customStyles = {
|
||||
multiValue: (provided) => ({
|
||||
...provided,
|
||||
borderRadius: '10px',
|
||||
backgroundColor: '#F5CD54',
|
||||
}),
|
||||
multiValueRemove: (base, state) => ({
|
||||
...base,
|
||||
color: state.isHovered ? 'initial' : base.color,
|
||||
opacity: '0.7',
|
||||
':hover': {
|
||||
backgroundColor: 'initial',
|
||||
color: 'initial',
|
||||
opacity: '1',
|
||||
},
|
||||
}),
|
||||
menuList: (provided) => ({
|
||||
...provided,
|
||||
height: '125px',
|
||||
zIndex: 999
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{marginTop: '5px'}}>
|
||||
<Select
|
||||
isMulti
|
||||
options={roles}
|
||||
onChange={handleChange}
|
||||
value={selectedRoles}
|
||||
menuPosition={"fixed"}
|
||||
styles={customStyles}
|
||||
isDisabled={!rolesEnabled}
|
||||
/>
|
||||
{! enabled &&
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay"><span
|
||||
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Activate Two-Factor Authentication to enable this block.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RolesDropDown;
|
||||
@@ -0,0 +1,77 @@
|
||||
import DOMPurify from "dompurify";
|
||||
import {useEffect, useRef} from '@wordpress/element';
|
||||
import hoverTooltip from '../utils/hoverTooltip';
|
||||
|
||||
const SelectControl = (props) => {
|
||||
|
||||
const selectRef = useRef(null);
|
||||
|
||||
const disabledPropIsArray = Array.isArray(props.disabled);
|
||||
let disabledOptionsArray = (disabledPropIsArray ? props.disabled : false);
|
||||
let disabledSelectPropBoolean = (disabledPropIsArray === false && props.disabled);
|
||||
let disabledSelectViaFieldConfig = (props.field.disabled === true);
|
||||
|
||||
let selectDisabled = (
|
||||
disabledSelectViaFieldConfig
|
||||
|| disabledSelectPropBoolean
|
||||
);
|
||||
|
||||
let tooltipText = '';
|
||||
let emptyValues = [undefined, null, ''];
|
||||
|
||||
if (selectDisabled
|
||||
&& props.field.hasOwnProperty('disabledTooltipHoverText')
|
||||
&& !emptyValues.includes(props.field.disabledTooltipHoverText)
|
||||
) {
|
||||
tooltipText = props.field.disabledTooltipHoverText;
|
||||
}
|
||||
|
||||
hoverTooltip(
|
||||
selectRef,
|
||||
(selectDisabled && (tooltipText !== '')),
|
||||
tooltipText
|
||||
);
|
||||
|
||||
// Add effect to disable the select element when the selectDisabled state changes
|
||||
useEffect(() => {
|
||||
if (selectRef.current) {
|
||||
selectRef.current.disabled = selectDisabled;
|
||||
}
|
||||
}, [disabledSelectViaFieldConfig, selectDisabled]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="components-base-control">
|
||||
<div className="components-base-control__field">
|
||||
<div data-wp-component="HStack" className="components-flex components-select-control">
|
||||
<label htmlFor={props.field.id} className="components-toggle-control__label"
|
||||
style={props.style && props.style.label ? props.style.label : undefined}>{props.label}</label>
|
||||
<select
|
||||
ref={selectRef}
|
||||
disabled={selectDisabled}
|
||||
value={props.value}
|
||||
onChange={(e) => props.onChangeHandler(e.target.value)}
|
||||
style={props.style && props.style.select ? props.style.select : undefined}
|
||||
>
|
||||
{props.options.map((option, i) => (
|
||||
<option
|
||||
key={'option-' + i}
|
||||
value={option.value}
|
||||
disabled={disabledOptionsArray && disabledOptionsArray.includes(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{props.field.comment && (
|
||||
<div className="rsssl-comment" dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(props.field.comment) }} ></div>
|
||||
/* nosemgrep: react-dangerouslysetinnerhtml */
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectControl;
|
||||
@@ -0,0 +1,211 @@
|
||||
import {useState, useEffect} from '@wordpress/element';
|
||||
import SettingsPlaceholder from '../Placeholder/SettingsPlaceholder';
|
||||
import {in_array} from '../utils/lib';
|
||||
import SettingsGroup from './SettingsGroup';
|
||||
import Help from './Help';
|
||||
import useFields from './FieldsData';
|
||||
import useMenu from '../Menu/MenuData';
|
||||
import {__} from '@wordpress/i18n';
|
||||
import useLetsEncryptData from '../LetsEncrypt/letsEncryptData';
|
||||
import ErrorBoundary from "../utils/ErrorBoundary";
|
||||
|
||||
/**
|
||||
* Renders the selected settings
|
||||
*
|
||||
*/
|
||||
const Settings = () => {
|
||||
const [noticesExpanded, setNoticesExpanded] = useState(true);
|
||||
const {
|
||||
progress,
|
||||
fieldsLoaded,
|
||||
saveFields,
|
||||
fields,
|
||||
nextButtonDisabled,
|
||||
} = useFields();
|
||||
const {
|
||||
subMenuLoaded,
|
||||
subMenu,
|
||||
selectedSubMenuItem,
|
||||
selectedMainMenuItem,
|
||||
nextMenuItem,
|
||||
previousMenuItem,
|
||||
} = useMenu();
|
||||
const {setRefreshTests} = useLetsEncryptData();
|
||||
const toggleNotices = () => {
|
||||
setNoticesExpanded(!noticesExpanded);
|
||||
};
|
||||
|
||||
const isTestsOnlyMenu = () => {
|
||||
const {menu_items: menuItems} = subMenu;
|
||||
for (const menuItem of menuItems) {
|
||||
if (menuItem.id === selectedSubMenuItem && menuItem.tests_only) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const saveData = async (isSaveAndContinueButton) => {
|
||||
if (!isSaveAndContinueButton && isTestsOnlyMenu()) {
|
||||
setRefreshTests(true);
|
||||
} else if (isSaveAndContinueButton) {
|
||||
await saveFields(true, false);
|
||||
} else {
|
||||
await saveFields(true, true);
|
||||
}
|
||||
};
|
||||
|
||||
const {menu_items: menuItems} = subMenu;
|
||||
if (!subMenuLoaded || !fieldsLoaded || menuItems.length === 0) {
|
||||
return (
|
||||
<SettingsPlaceholder/>
|
||||
);
|
||||
}
|
||||
|
||||
let selectedFields = fields.filter(
|
||||
field => field.menu_id === selectedSubMenuItem);
|
||||
let groups = [];
|
||||
for (const selectedField of selectedFields) {
|
||||
if (!in_array(selectedField.group_id, groups)) {
|
||||
groups.push(selectedField.group_id);
|
||||
}
|
||||
}
|
||||
|
||||
//convert progress notices to an array useful for the help blocks
|
||||
let notices = [];
|
||||
for (const notice of progress.notices) {
|
||||
let noticeIsLinkedToField = false;
|
||||
|
||||
//notices that are linked to a field. Only in case of warnings.
|
||||
if (notice.show_with_options) {
|
||||
let noticeFields = selectedFields.filter(
|
||||
field => notice.show_with_options.includes(field.id));
|
||||
noticeIsLinkedToField = noticeFields.length > 0;
|
||||
}
|
||||
//notices that are linked to a menu id.
|
||||
if (noticeIsLinkedToField || notice.menu_id === selectedSubMenuItem) {
|
||||
let help = {};
|
||||
help.title = notice.output.title ? notice.output.title : false;
|
||||
help.label = notice.output.label;
|
||||
help.id = notice.id;
|
||||
help.text = notice.output.msg;
|
||||
help.url = notice.output.url;
|
||||
help.linked_field = notice.show_with_option;
|
||||
notices.push(help);
|
||||
}
|
||||
}
|
||||
|
||||
//help items belonging to a field
|
||||
//if field is hidden, hide the notice as well
|
||||
for (const notice of selectedFields.filter(
|
||||
field => field.help && !field.conditionallyDisabled)) {
|
||||
let help = notice.help;
|
||||
//check if the notices array already includes this help item
|
||||
let existingNotices = notices.filter(
|
||||
noticeItem => noticeItem.id && noticeItem.id === help.id);
|
||||
if (existingNotices.length === 0) {
|
||||
// if (!help.id ) help['id'] = notice.id;
|
||||
notices.push(notice.help);
|
||||
}
|
||||
}
|
||||
let continueLink = nextButtonDisabled
|
||||
? `#${selectedMainMenuItem}/${selectedSubMenuItem}`
|
||||
: `#${selectedMainMenuItem}/${nextMenuItem}`;
|
||||
// let btnSaveText = isTestsOnlyMenu() ? __('Refresh', 'really-simple-ssl') :
|
||||
// __('Save', 'really-simple-ssl');
|
||||
let btnSaveText = __('Save', 'really-simple-ssl');
|
||||
for (const menuItem of menuItems) {
|
||||
if (menuItem.id === selectedSubMenuItem && menuItem.tests_only) {
|
||||
btnSaveText = __('Refresh', 'really-simple-ssl');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-wizard-settings">
|
||||
{groups.map((group, i) =>
|
||||
<SettingsGroup key={'settingsGroup-' + i} index={i} group={group}
|
||||
fields={selectedFields}/>)
|
||||
}
|
||||
<div className="rsssl-grid-item-footer-container">
|
||||
<ScrollProgress/>
|
||||
<div className="rsssl-grid-item-footer">
|
||||
<div className={'rsssl-grid-item-footer-buttons'}>
|
||||
{/*This will be shown only if current step is not the first one*/}
|
||||
{selectedSubMenuItem !== menuItems[0].id &&
|
||||
<a className="rsssl-previous"
|
||||
href={`#${selectedMainMenuItem}/${previousMenuItem}`}>
|
||||
{__('Previous', 'really-simple-ssl')}
|
||||
</a>
|
||||
}
|
||||
<button
|
||||
className="button button-secondary"
|
||||
onClick={(e) => saveData(false)}>
|
||||
{btnSaveText}
|
||||
</button>
|
||||
{/*This will be shown only if current step is not the last one*/}
|
||||
{selectedSubMenuItem !==
|
||||
menuItems[menuItems.length - 1].id &&
|
||||
<>
|
||||
<button disabled={nextButtonDisabled}
|
||||
className="button button-primary"
|
||||
onClick={(e) => {saveData(true);window.location.href=continueLink;} }>
|
||||
{__('Save and continue', 'really-simple-ssl')}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rsssl-wizard-help">
|
||||
<div className="rsssl-help-header">
|
||||
<div className="rsssl-help-title rsssl-h4">
|
||||
{__("Notifications", "really-simple-ssl")}
|
||||
</div>
|
||||
<div className="rsssl-help-control" onClick={ () => toggleNotices() }>
|
||||
{!noticesExpanded && __("Expand all","really-simple-ssl")}
|
||||
{noticesExpanded && __("Collapse all","really-simple-ssl")}
|
||||
</div>
|
||||
</div>
|
||||
{ notices.map((field, i) => <ErrorBoundary key={'errorboundary-'+i} fallback={"Could not load notices"}>
|
||||
<Help noticesExpanded={noticesExpanded} index={i} help={field} fieldId={field.id}/>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
|
||||
};
|
||||
export default Settings;
|
||||
|
||||
export const ScrollProgress = () => {
|
||||
// calculate the scroll progress
|
||||
const [scrollProgress, setScrollProgress] = useState(0);
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', () => {
|
||||
let scrollableHeight = document.documentElement.scrollHeight -
|
||||
document.documentElement.clientHeight;
|
||||
let scrollProgressPercentage = Math.round(
|
||||
(window.scrollY / scrollableHeight) * 100);
|
||||
// start at 5% and end at 100%
|
||||
scrollProgressPercentage = Math.max(5, scrollProgressPercentage);
|
||||
setScrollProgress(scrollProgressPercentage);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// if you can't scroll return null
|
||||
if (document.documentElement.scrollHeight <=
|
||||
document.documentElement.clientHeight) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
// add width to span
|
||||
<span className={'rsssl-grid-item-footer-scroll-progress-container'}>
|
||||
<span className={'rsssl-grid-item-footer-scroll-progress'}
|
||||
style={{width: scrollProgress + '%'}}>{scrollProgress}%</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,189 @@
|
||||
import Hyperlink from "../utils/Hyperlink";
|
||||
import getAnchor from "../utils/getAnchor";
|
||||
import {__} from '@wordpress/i18n';
|
||||
import * as rsssl_api from "../utils/api";
|
||||
import useFields from "../Settings/FieldsData";
|
||||
import useMenu from "../Menu/MenuData";
|
||||
import useLicense from "./License/LicenseData";
|
||||
import filterData from "./FilterData";
|
||||
import {useEffect, useState} from '@wordpress/element';
|
||||
import ErrorBoundary from "../utils/ErrorBoundary";
|
||||
import PremiumOverlay from "./PremiumOverlay";
|
||||
|
||||
/**
|
||||
* Render a grouped block of settings
|
||||
*/
|
||||
const SettingsGroup = (props) => {
|
||||
|
||||
const {fields} = useFields();
|
||||
const {selectedFilter, setSelectedFilter} = filterData();
|
||||
const {licenseStatus} = useLicense();
|
||||
const {selectedSubMenuItem, subMenu} = useMenu();
|
||||
const [Field, setField] = useState(null);
|
||||
const [updatedIntro, setUpdatedIntro] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
import("./Field").then(({default: Field}) => {
|
||||
setField(() => Field);
|
||||
});
|
||||
if (activeGroup && activeGroup.intro && typeof activeGroup.intro === 'object') {
|
||||
setUpdatedIntro(activeGroup.intro[selectedFilter[filterId]]);
|
||||
}
|
||||
|
||||
}, [selectedFilter]);
|
||||
|
||||
/*
|
||||
* On reset of LE, send this info to the back-end, and redirect to the first step.
|
||||
* reload to ensure that.
|
||||
*/
|
||||
const handleLetsEncryptReset = (e) => {
|
||||
e.preventDefault();
|
||||
rsssl_api.runLetsEncryptTest('reset').then((response) => {
|
||||
window.location.href = window.location.href.replace(/#letsencrypt.*/, '&r=' + (+new Date()) + '#letsencrypt/le-system-status');
|
||||
});
|
||||
}
|
||||
|
||||
let selectedFields = [];
|
||||
//get all fields with group_id props.group_id
|
||||
for (const selectedField of fields) {
|
||||
if (selectedField.group_id === props.group) {
|
||||
selectedFields.push(selectedField);
|
||||
}
|
||||
}
|
||||
|
||||
let activeGroup;
|
||||
for (const item of subMenu.menu_items) {
|
||||
if (item.id === selectedSubMenuItem && item.hasOwnProperty('groups')) {
|
||||
for (const group of item.groups) {
|
||||
if (group.group_id === props.group) {
|
||||
activeGroup = group;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (activeGroup) break; // Exit the loop once a match is found.
|
||||
}
|
||||
|
||||
// If activeGroup is not set, then default to the parent menu item.
|
||||
if (!activeGroup) {
|
||||
for (const item of subMenu.menu_items) {
|
||||
if (item.id === selectedSubMenuItem) {
|
||||
activeGroup = item;
|
||||
break;
|
||||
}
|
||||
// Handle the case where there are nested menu items.
|
||||
if (item.menu_items) {
|
||||
const nestedItem = item.menu_items.find(menuItem => menuItem.id === selectedSubMenuItem);
|
||||
if (nestedItem) {
|
||||
activeGroup = nestedItem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for nested groups in the activeGroup.
|
||||
if (activeGroup && activeGroup.groups) {
|
||||
const nestedGroup = activeGroup.groups.find(group => group.group_id === props.group);
|
||||
if (nestedGroup) {
|
||||
activeGroup = nestedGroup;
|
||||
} else {
|
||||
const nestedGroup = activeGroup.groups.find(group => group.group_id === props.group);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
let disabled = licenseStatus !== 'valid' && activeGroup.premium;
|
||||
//if a feature can only be used on networkwide or single site setups, pass that info here.
|
||||
let networkwide_error = !rsssl_settings.networkwide_active && activeGroup.networkwide_required;
|
||||
let helplinkText = activeGroup.helpLink_text ? activeGroup.helpLink_text : __("Instructions", "really-simple-ssl");
|
||||
let anchor = getAnchor('main');
|
||||
let disabledClass = disabled || networkwide_error ? 'rsssl-disabled' : '';
|
||||
const filterId = "rsssl-group-filter-" + activeGroup.id;
|
||||
//filter out all fields that are not visible
|
||||
selectedFields = selectedFields.filter((field) => {
|
||||
if (field.hasOwnProperty('visible')) {
|
||||
return field.visible;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
//if there are no visible fields, return null
|
||||
if (selectedFields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={"rsssl-grid-item rsssl-" + activeGroup.id + ' ' + disabledClass}>
|
||||
{activeGroup.title && <div className="rsssl-grid-item-header">
|
||||
<h3 className="rsssl-h4">{activeGroup.title}</h3>
|
||||
{activeGroup.groupFilter && (
|
||||
<div className="rsssl-grid-item-controls">
|
||||
<select
|
||||
className="rsssl-group-filter"
|
||||
id={filterId}
|
||||
name={filterId}
|
||||
value={selectedFilter[filterId]}
|
||||
onChange={(e) => {
|
||||
const selectedValue = e.target.value;
|
||||
setSelectedFilter(selectedValue, filterId);
|
||||
}}
|
||||
>
|
||||
{activeGroup.groupFilter.options.map((option) => (
|
||||
//if the value is equal to the selected value, set it as selected
|
||||
<option
|
||||
key={'option-'+option.id}
|
||||
value={option.id}
|
||||
>
|
||||
{option.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{!activeGroup.groupFilter && activeGroup.helpLink && anchor !== 'letsencrypt' && (
|
||||
<div className="rsssl-grid-item-controls">
|
||||
<Hyperlink
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="rsssl-helplink"
|
||||
text={helplinkText}
|
||||
url={activeGroup.helpLink}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{anchor === 'letsencrypt' && <div className="rsssl-grid-item-controls">
|
||||
<a href="#" className="rsssl-helplink"
|
||||
onClick={(e) => handleLetsEncryptReset(e)}>{__("Reset Let's Encrypt", "really-simple-ssl")}</a>
|
||||
</div>}
|
||||
</div>}
|
||||
<div className="rsssl-grid-item-content">
|
||||
{(activeGroup.intro && typeof activeGroup.intro === 'string') && <ErrorBoundary fallback={"Could not load group intro"}>
|
||||
{(activeGroup.intro && typeof activeGroup.intro === 'string') && <div className="rsssl-settings-block-intro">{activeGroup.intro}</div>}
|
||||
{(activeGroup.intro && typeof activeGroup.intro === 'object') && <div className="rsssl-settings-block-intro">{updatedIntro}</div>}
|
||||
</ErrorBoundary>}
|
||||
|
||||
{Field && selectedFields.map((field, i) =>
|
||||
<Field key={"selectedFields-" + i} index={i} field={field} fields={selectedFields}/>
|
||||
)}
|
||||
</div>
|
||||
{disabled && !networkwide_error && <PremiumOverlay
|
||||
msg={activeGroup.premium_text}
|
||||
title={activeGroup.premium_title ? activeGroup.premium_title : activeGroup.title}
|
||||
upgrade={activeGroup.upgrade}
|
||||
url={activeGroup.upgrade}
|
||||
/>}
|
||||
|
||||
{networkwide_error && <div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay">
|
||||
<span
|
||||
className="rsssl-task-status rsssl-warning">{__("Network feature", "really-simple-ssl")}</span>
|
||||
<span>{__("This feature is only available networkwide.", "really-simple-ssl")}<Hyperlink
|
||||
target="_blank" rel="noopener noreferrer" text={__("Network settings", "really-simple-ssl")}
|
||||
url={rsssl_settings.network_link}/></span>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsGroup
|
||||
@@ -0,0 +1,52 @@
|
||||
import {TextareaControl,} from '@wordpress/components';
|
||||
import {__} from '@wordpress/i18n';
|
||||
import * as rsssl_api from "../utils/api";
|
||||
import {useState} from "@wordpress/element";
|
||||
import {addUrlRef} from "../utils/AddUrlRef";
|
||||
|
||||
const Support = () => {
|
||||
const [message, setMessage] = useState('');
|
||||
const [sending, setSending] = useState(false);
|
||||
|
||||
const onChangeHandler = (message) => {
|
||||
setMessage(message);
|
||||
}
|
||||
|
||||
const onClickHandler = () => {
|
||||
setSending(true);
|
||||
return rsssl_api.runTest('supportData', 'refresh').then( ( response ) => {
|
||||
let encodedMessage = message.replace(/(?:\r\n|\r|\n)/g, '--br--');
|
||||
let url = 'https://really-simple-ssl.com/support'
|
||||
+'?customername=' + encodeURIComponent(response.customer_name)
|
||||
+ '&email=' + response.email
|
||||
+ '&domain=' + response.domain
|
||||
+ '&scanresults=' + encodeURIComponent(response.scan_results)
|
||||
+ '&licensekey=' + encodeURIComponent(response.license_key)
|
||||
+ '&supportrequest=' + encodeURIComponent(encodedMessage)
|
||||
+ '&htaccesscontents=' + encodeURIComponent(response.htaccess_contents)
|
||||
+ '&debuglog=' + encodeURIComponent(response.system_status);
|
||||
url = addUrlRef(url);
|
||||
window.location.assign(url);
|
||||
});
|
||||
}
|
||||
|
||||
let disabled = sending || message.length===0;
|
||||
return (
|
||||
<>
|
||||
<TextareaControl
|
||||
disabled={sending}
|
||||
placeholder={__("Type your question here","really-simple-ssl")}
|
||||
onChange={ ( message ) => onChangeHandler(message) }
|
||||
/>
|
||||
<button
|
||||
className={"button button-secondary"}
|
||||
disabled={disabled}
|
||||
onClick={ ( e ) => onClickHandler(e) }>
|
||||
{ __( 'Send', 'really-simple-ssl' ) }
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default Support;
|
||||
@@ -0,0 +1,41 @@
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import {produce} from "immer";
|
||||
const useRolesData = create(( set, get ) => ({
|
||||
roles: [],
|
||||
rolesLoaded:false,
|
||||
fetchRoles: async ( id ) => {
|
||||
try {
|
||||
// Fetch the roles from the server using rsssl_api.getUserRoles()
|
||||
const response = await rsssl_api.doAction('get_roles', { id: id });
|
||||
|
||||
// Handle the response
|
||||
if ( !response ) {
|
||||
console.error('No response received from the server.');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = response.roles;
|
||||
if (typeof data !== 'object') {
|
||||
console.error('Invalid data received in the server response. Expected an object.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert the object to an array
|
||||
const dataArray = Object.values(data);
|
||||
|
||||
// Format the data into options array for react-select
|
||||
|
||||
const formattedData = dataArray.map((role, index) => ({ value: role, label: role.charAt(0).toUpperCase() + role.slice(1) }));
|
||||
// Set the roles state with formatted data
|
||||
set({roles: formattedData,rolesLoaded:true });
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
export default useRolesData;
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
import {__} from '@wordpress/i18n';
|
||||
import {useEffect, useState} from '@wordpress/element';
|
||||
import DataTable, {createTheme} from "react-data-table-component";
|
||||
import useFields from "../FieldsData";
|
||||
import TwoFaDataTableStore from "./TwoFaDataTableStore";
|
||||
import FilterData from "../FilterData";
|
||||
|
||||
const DynamicDataTable = (props) => {
|
||||
const {
|
||||
resetUserMethod,
|
||||
hardResetUser,
|
||||
handleUsersTableFilter,
|
||||
totalRecords,
|
||||
DynamicDataTable,
|
||||
setDataLoaded,
|
||||
dataLoaded,
|
||||
fetchDynamicData,
|
||||
handleTableSort,
|
||||
processing
|
||||
} = TwoFaDataTableStore();
|
||||
|
||||
const {
|
||||
setSelectedFilter,
|
||||
getCurrentFilter
|
||||
} = FilterData();
|
||||
|
||||
const moduleName = 'rsssl-group-filter-two_fa_users';
|
||||
|
||||
let field = props.field;
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [reloadWhenSaved, setReloadWhenSaved] = useState(false);
|
||||
const {fields, getFieldValue, changedFields} = useFields();
|
||||
const [rowsSelected, setRowsSelected] = useState([]);
|
||||
const [rowCleared, setRowCleared] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dataLoaded) {
|
||||
fetchDynamicData();
|
||||
} else {
|
||||
setData(DynamicDataTable);
|
||||
}
|
||||
}, [dataLoaded, DynamicDataTable]);
|
||||
|
||||
useEffect(() => {
|
||||
setReloadWhenSaved(true);
|
||||
setDataLoaded(false);
|
||||
}, [getFieldValue('two_fa_forced_roles'), getFieldValue('two_fa_optional_roles'), getFieldValue('two_fa_forced_roles_totp'), getFieldValue('two_fa_optional_roles_totp')]);
|
||||
|
||||
useEffect(() => {
|
||||
if (reloadWhenSaved) {
|
||||
if (changedFields.length === 0) {
|
||||
setDataLoaded(false);
|
||||
setReloadWhenSaved(false);
|
||||
}
|
||||
}
|
||||
}, [reloadWhenSaved]);
|
||||
|
||||
const handleTableSearch = (value, columns) => {
|
||||
const search = value.toLowerCase();
|
||||
const searchColumns = columns;
|
||||
const filteredData = DynamicDataTable.filter((item) => {
|
||||
return searchColumns.some((column) => {
|
||||
return item[column].toString().toLowerCase().includes(search);
|
||||
});
|
||||
});
|
||||
setData(filteredData);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (dataLoaded) {
|
||||
const currentFilter = getCurrentFilter(moduleName);
|
||||
if (!currentFilter) {
|
||||
setSelectedFilter('all', moduleName);
|
||||
}
|
||||
setRowCleared(true);
|
||||
handleUsersTableFilter('rsssl_two_fa_status', currentFilter);
|
||||
}
|
||||
}, [getCurrentFilter(moduleName)]);
|
||||
|
||||
useEffect(() => {
|
||||
let enabledEmailRoles = getFieldValue('two_fa_enabled_roles_email');
|
||||
let enabledTotpRoles = getFieldValue('two_fa_enabled_roles_totp');
|
||||
let enabledRoles = enabledEmailRoles.concat(enabledTotpRoles);
|
||||
setEnabled(getFieldValue('login_protection_enabled'));
|
||||
}, [fields]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dataLoaded || enabled !== (getFieldValue('two_fa_enabled_email') || getFieldValue('two_fa_enabled_totp'))) {
|
||||
setDataLoaded(false);
|
||||
}
|
||||
}, [getFieldValue('two_fa_enabled'), getFieldValue('two_fa_enabled_totp')]);
|
||||
|
||||
const allAreForced = (users) => {
|
||||
let forcedRoles = getFieldValue('two_fa_forced_roles');
|
||||
let forcedRolesTotp = getFieldValue('two_fa_forced_roles_totp');
|
||||
if (!Array.isArray(forcedRoles)) {
|
||||
forcedRoles = [];
|
||||
}
|
||||
if (!Array.isArray(forcedRolesTotp)) {
|
||||
forcedRolesTotp = [];
|
||||
}
|
||||
if (Array.isArray(users)) {
|
||||
for (const user of users) {
|
||||
if (user.user_role === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (user.rsssl_two_fa_providers.toLowerCase() === 'none') {
|
||||
return true;
|
||||
}
|
||||
if (user.status_for_user.toLowerCase() === 'active' || user.status_for_user.toLowerCase() === 'disabled' || user.status_for_user.toLowerCase() === 'expired') {
|
||||
return false;
|
||||
}
|
||||
if (!forcedRoles.includes(user.user_role.toLowerCase()) && !forcedRolesTotp.includes(user.user_role.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
if (users.user_role === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (users.status_for_user.toLowerCase() === 'active' || users.status_for_user.toLowerCase() === 'disabled' || users.status_for_user.toLowerCase() === 'expired') {
|
||||
return false;
|
||||
}
|
||||
return (forcedRoles.includes(users.user_role.toLowerCase()) || forcedRolesTotp.includes(users.user_role.toLowerCase()));
|
||||
}
|
||||
}
|
||||
|
||||
const allAreOpen = (users) => {
|
||||
if (Array.isArray(users)) {
|
||||
for (const user of users) {
|
||||
if (user.status_for_user.toLowerCase() !== 'open') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return users.status_for_user.toLowerCase() === 'open';
|
||||
}
|
||||
}
|
||||
|
||||
const buildColumn = (column) => {
|
||||
return {
|
||||
name: column.name,
|
||||
column: column.column,
|
||||
sortable: column.sortable,
|
||||
searchable: column.searchable,
|
||||
width: column.width,
|
||||
visible: column.visible,
|
||||
selector: row => row[column.column],
|
||||
};
|
||||
}
|
||||
|
||||
let columns = [];
|
||||
|
||||
field.columns.forEach(function (item, i) {
|
||||
let newItem = { ...item, key: item.column };
|
||||
newItem = buildColumn(newItem);
|
||||
newItem.visible = newItem.visible ?? true;
|
||||
columns.push(newItem);
|
||||
});
|
||||
|
||||
let searchableColumns = columns
|
||||
.filter(column => column.searchable)
|
||||
.map(column => column.column);
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light');
|
||||
|
||||
const handleReset = async (users) => {
|
||||
let resetRolesEmail = getFieldValue('two_fa_forced_roles_email');
|
||||
let resetRolesTotp = getFieldValue('two_fa_forced_roles_totp');
|
||||
resetRolesEmail = Array.isArray(resetRolesEmail) ? resetRolesEmail : [resetRolesEmail];
|
||||
resetRolesTotp = Array.isArray(resetRolesTotp) ? resetRolesTotp : [resetRolesTotp];
|
||||
|
||||
const resetRoles = resetRolesEmail.concat(resetRolesTotp);
|
||||
|
||||
if (Array.isArray(users)) {
|
||||
for (const user of users) {
|
||||
await hardResetUser(user.id, resetRoles, user.user_role.toLowerCase());
|
||||
}
|
||||
} else {
|
||||
await hardResetUser(users.id, resetRoles, users.user_role.toLowerCase());
|
||||
}
|
||||
|
||||
setDataLoaded(false);
|
||||
setRowsSelected([]);
|
||||
setRowCleared(true);
|
||||
}
|
||||
|
||||
const handleSelection = (state) => {
|
||||
setRowsSelected(state.selectedRows);
|
||||
}
|
||||
|
||||
const capitalizeFirstLetter = (string) => {
|
||||
//if the string is totp we capitlize it
|
||||
if (string === 'totp') {
|
||||
return string.toUpperCase();
|
||||
}
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
let resetDisabled = allAreForced(rowsSelected) || allAreOpen(rowsSelected);
|
||||
let inputData = data ? data : [];
|
||||
const paginatedData = inputData.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage);
|
||||
let displayData = [];
|
||||
paginatedData.forEach(user => {
|
||||
let recordCopy = { ...user }
|
||||
recordCopy.user = capitalizeFirstLetter(user.user);
|
||||
recordCopy.user_role = capitalizeFirstLetter(user.user_role);
|
||||
recordCopy.status_for_user = __(capitalizeFirstLetter(user.status_for_user), 'really-simple-ssl');
|
||||
recordCopy.rsssl_two_fa_providers = __(capitalizeFirstLetter(user.rsssl_two_fa_providers), 'really-simple-ssl');
|
||||
let btnDisabled = allAreForced(user) || allAreOpen(user);
|
||||
recordCopy.resetControl = <button disabled={btnDisabled}
|
||||
className="button button-red rsssl-action-buttons__button"
|
||||
onClick={() => handleReset(user)}
|
||||
>
|
||||
{__("Reset", "really-simple-ssl")}
|
||||
</button>
|
||||
displayData.push(recordCopy);
|
||||
});
|
||||
const CustomLoader = () => (
|
||||
<div className="custom-loader">
|
||||
<div className="dot"></div>
|
||||
<div className="dot"></div>
|
||||
<div className="dot"></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-container" style={{ marginTop: "20px" }}>
|
||||
<div>
|
||||
{/* Reserved for actions left */}
|
||||
</div>
|
||||
<div className="rsssl-search-bar">
|
||||
<div className="rsssl-search-bar__inner">
|
||||
<div className="rsssl-search-bar__icon"></div>
|
||||
<input
|
||||
type="text"
|
||||
className="rsssl-search-bar__input"
|
||||
placeholder={__("Search", "really-simple-ssl")}
|
||||
onChange={event => handleTableSearch(event.target.value, searchableColumns)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{rowsSelected.length > 0 && (
|
||||
<div style={{ marginTop: '1em', marginBottom: '1em' }}>
|
||||
<div className={"rsssl-multiselect-datatable-form rsssl-primary"}>
|
||||
<div>
|
||||
{__("You have selected %s users", "really-simple-ssl").replace("%s", rowsSelected.length)}
|
||||
</div>
|
||||
<div className="rsssl-action-buttons">
|
||||
<div className="rsssl-action-buttons__inner">
|
||||
<button disabled={resetDisabled || processing}
|
||||
className="button button-red rsssl-action-buttons__button"
|
||||
onClick={() => handleReset(rowsSelected)}
|
||||
>
|
||||
{__("Reset", "really-simple-ssl")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={displayData}
|
||||
dense
|
||||
pagination
|
||||
paginationServer={true}
|
||||
onChangePage={page => {
|
||||
setCurrentPage(page);
|
||||
}}
|
||||
onChangeRowsPerPage={rows => {
|
||||
setRowsPerPage(rows);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
paginationTotalRows={data.length}
|
||||
paginationRowsPerPageOptions={[5, 25, 50, 100]}
|
||||
paginationPerPage={rowsPerPage}
|
||||
progressPending={processing} // Show loading indicator
|
||||
progressComponent={<CustomLoader />}
|
||||
onSort={handleTableSort}
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
selectableRows
|
||||
selectableRowsHighlight={true}
|
||||
onSelectedRowsChange={handleSelection}
|
||||
clearSelectedRows={rowCleared}
|
||||
theme="really-simple-plugins"
|
||||
customStyles={customStyles}
|
||||
/>
|
||||
{!enabled &&
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay">
|
||||
<span className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span>
|
||||
<span>{__('Activate Two-Factor Authentication and one method to enable this block.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default DynamicDataTable;
|
||||
@@ -0,0 +1,144 @@
|
||||
/* Creates A Store For Risk Data using Zustand */
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import {produce} from "immer";
|
||||
import apiFetch from "@wordpress/api-fetch";
|
||||
|
||||
const DynamicDataTableStore = create((set, get) => ({
|
||||
processing: false,
|
||||
dataLoaded: false,
|
||||
pagination: {},
|
||||
dataActions: {currentPage:1, currentRowsPerPage:5, filterValue: 'all',filterColumn: 'rsssl_two_fa_status'},
|
||||
totalRecords:0,
|
||||
DynamicDataTable: [],
|
||||
|
||||
setDataLoaded: (dataLoaded) => set((state) => ({ ...state, dataLoaded: dataLoaded })),
|
||||
resetUserMethod: async (id, optionalRoles, currentRole) => {
|
||||
if (get().processing) {
|
||||
return;
|
||||
}
|
||||
if ( optionalRoles.includes(currentRole) ) {
|
||||
set({processing: true});
|
||||
set({dataLoaded: false});
|
||||
const response = await apiFetch({
|
||||
path: `/wp/v2/users/${id}`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
meta: {
|
||||
rsssl_two_fa_status_email: 'open',
|
||||
rsssl_two_fa_status_totp: 'open',
|
||||
},
|
||||
_wpnonce: rsssl_settings.nonce,
|
||||
},
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
set({processing: false});
|
||||
set({dataLoaded: true});
|
||||
}
|
||||
},
|
||||
hardResetUser: async (id) => {
|
||||
if (get().processing) return;
|
||||
set({processing: true});
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
'two_fa_reset_user',
|
||||
{id}
|
||||
);
|
||||
if (response) {
|
||||
set(state => ({
|
||||
...state,
|
||||
processing: false,
|
||||
}));
|
||||
// Return the response for the calling function to use
|
||||
return response;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
fetchDynamicData: async () => {
|
||||
if (get().processing) return;
|
||||
set({processing: true});
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
'two_fa_table',
|
||||
get().dataActions
|
||||
);
|
||||
if (response && response.data) {
|
||||
set(state => ({
|
||||
...state,
|
||||
DynamicDataTable: response.data,
|
||||
dataLoaded: true,
|
||||
processing: false,
|
||||
pagination: response.pagination,
|
||||
totalRecords: response.totalRecords,
|
||||
}));
|
||||
// Return the response for the calling function to use
|
||||
return response;
|
||||
} else {
|
||||
set(state => ({
|
||||
...state,
|
||||
processing: false,
|
||||
dataLoaded: true,
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
|
||||
handleTableSearch: async (search, searchColumns) => {
|
||||
const typingTimer = setTimeout(async () => {
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, search, searchColumns};
|
||||
}));
|
||||
await get().fetchDynamicData();
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(typingTimer);
|
||||
};
|
||||
},
|
||||
|
||||
handlePageChange: async (page, pageSize) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, currentPage: page};
|
||||
})
|
||||
);
|
||||
await get().fetchDynamicData();
|
||||
},
|
||||
|
||||
handleRowsPerPageChange: async (currentRowsPerPage, currentPage) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, currentRowsPerPage, currentPage};
|
||||
})
|
||||
);
|
||||
await get().fetchDynamicData();
|
||||
},
|
||||
|
||||
//this handles all pagination and sorting
|
||||
handleTableSort: async (sortColumn, sortDirection) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, sortColumn: sortColumn.column, sortDirection};
|
||||
})
|
||||
);
|
||||
await get().fetchDynamicData();
|
||||
},
|
||||
|
||||
handleUsersTableFilter: async (column, filterValue) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, filterColumn: column, filterValue};
|
||||
})
|
||||
);
|
||||
// Fetch the data again
|
||||
await get().fetchDynamicData();
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
export default DynamicDataTableStore;
|
||||
@@ -0,0 +1,141 @@
|
||||
import {useRef, useEffect, useState} from '@wordpress/element';
|
||||
import Select from 'react-select';
|
||||
import useFields from "../FieldsData";
|
||||
import useRolesData from './RolesStore';
|
||||
import hoverTooltip from "../../utils/hoverTooltip";
|
||||
import {__} from "@wordpress/i18n";
|
||||
|
||||
/**
|
||||
* TwoFaEnabledDropDown component represents a dropdown select for excluding roles
|
||||
* from two-factor authentication email.
|
||||
*/
|
||||
const TwoFaEnabledDropDown = (props) => {
|
||||
const {fetchRoles, roles, rolesLoaded} = useRolesData();
|
||||
const [selectedRoles, setSelectedRoles] = useState([]);
|
||||
const [otherRoles, setOtherRoles] = useState([]);
|
||||
const { updateField, getFieldValue, setChangedField, getField, fieldsLoaded, saveFields } = useFields();
|
||||
const selectRef = useRef(null);
|
||||
|
||||
const loginProtectionEnabled = getFieldValue('login_protection_enabled');
|
||||
|
||||
let disabledSelectPropBoolean = (props.disabled === true);
|
||||
let disabledSelectViaFieldConfig = (props.field.disabled === true);
|
||||
|
||||
let selectDisabled = (
|
||||
disabledSelectViaFieldConfig
|
||||
|| disabledSelectPropBoolean
|
||||
);
|
||||
|
||||
let tooltipText = '';
|
||||
let emptyValues = [undefined, null, ''];
|
||||
|
||||
if (selectDisabled
|
||||
&& props.field.hasOwnProperty('disabledTooltipText')
|
||||
&& !emptyValues.includes(props.field.disabledTooltipHoverText)
|
||||
) {
|
||||
tooltipText = props.field.disabledTooltipHoverText;
|
||||
}
|
||||
|
||||
hoverTooltip(
|
||||
selectRef,
|
||||
(selectDisabled && (tooltipText !== '')),
|
||||
tooltipText
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (getFieldValue('login_protection_enabled') === 1 && props.field.id === 'two_fa_enabled_roles_totp') {
|
||||
setChangedField(props.field.id, props.field.value);
|
||||
saveFields(true, false);
|
||||
}
|
||||
}, [loginProtectionEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rolesLoaded) {
|
||||
fetchRoles(props.field.id);
|
||||
}
|
||||
}, [rolesLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.field.id) {
|
||||
let otherField = getField(props.field.id);
|
||||
let roles = Array.isArray(otherField.value) ? otherField.value : [];
|
||||
setOtherRoles(roles);
|
||||
}
|
||||
}, [selectedRoles, getField(props.field.id)]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.field.id) {
|
||||
let otherField = getField(props.field.id);
|
||||
let roles = Array.isArray(otherField.value) ? otherField.value : [];
|
||||
setSelectedRoles(roles.map((role) => ({ value: role, label: role.charAt(0).toUpperCase() + role.slice(1) })));
|
||||
}
|
||||
|
||||
if (!props.field.value) {
|
||||
setChangedField(props.field.id, props.field.default);
|
||||
updateField(props.field.id, props.field.default);
|
||||
setSelectedRoles(props.field.default.map((role) => ({ value: role, label: role.charAt(0).toUpperCase() + role.slice(1) })));
|
||||
}
|
||||
}, [fieldsLoaded]);
|
||||
|
||||
const handleChange = (selectedOptions) => {
|
||||
const rolesExcluded = selectedOptions.map(option => option.value);
|
||||
setSelectedRoles(selectedOptions);
|
||||
updateField(props.field.id, rolesExcluded);
|
||||
setChangedField(props.field.id, rolesExcluded);
|
||||
};
|
||||
|
||||
const customStyles = {
|
||||
multiValue: (provided) => ({
|
||||
...provided,
|
||||
borderRadius: '10px',
|
||||
backgroundColor: '#F5CD54',
|
||||
}),
|
||||
multiValueRemove: (base, state) => ({
|
||||
...base,
|
||||
color: state.isHovered ? 'initial' : base.color,
|
||||
opacity: '0.7',
|
||||
':hover': {
|
||||
backgroundColor: 'initial',
|
||||
color: 'initial',
|
||||
opacity: '1',
|
||||
},
|
||||
}),
|
||||
menuPortal: (base) => ({
|
||||
...base,
|
||||
zIndex: 30,
|
||||
}),
|
||||
};
|
||||
|
||||
const alreadySelected = selectedRoles.map(option => option.value);
|
||||
let filteredRoles = [];
|
||||
let inRolesInUse = [...alreadySelected, ...otherRoles];
|
||||
|
||||
roles.forEach((item) => {
|
||||
if (!inRolesInUse.includes(item.value)) {
|
||||
filteredRoles.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{marginTop: '5px'}} ref={selectRef}>
|
||||
<Select
|
||||
isMulti
|
||||
options={filteredRoles}
|
||||
onChange={handleChange}
|
||||
value={selectedRoles}
|
||||
menuPosition={"fixed"}
|
||||
styles={customStyles}
|
||||
isDisabled={selectDisabled}
|
||||
/>
|
||||
{! loginProtectionEnabled &&
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay"><span
|
||||
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Activate Two-Factor Authentication to enable this block.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TwoFaEnabledDropDown;
|
||||
@@ -0,0 +1,120 @@
|
||||
import {useRef, useEffect, useState} from '@wordpress/element';
|
||||
import Select from 'react-select';
|
||||
import useFields from "../FieldsData";
|
||||
import useRolesData from './RolesStore';
|
||||
import {__} from "@wordpress/i18n";
|
||||
import './select.scss';
|
||||
|
||||
/**
|
||||
* TwoFaRolesDropDown component represents a dropdown select for excluding roles
|
||||
* from two-factor authentication email.
|
||||
* @param {object} field - The field object containing information about the field.
|
||||
* @param forcedRoledId
|
||||
* @param optionalRolesId
|
||||
*/
|
||||
const TwoFaRolesDropDown = ({ field, forcedRoledId, optionalRolesId }) => {
|
||||
const {fetchRoles, roles, rolesLoaded} = useRolesData();
|
||||
const [selectedRoles, setSelectedRoles] = useState([]);
|
||||
const [otherRoles, setOtherRoles] = useState([]);
|
||||
const { updateField, getFieldValue, setChangedField, getField, fieldsLoaded, showSavedSettingsNotice, saveField } = useFields();
|
||||
// Reference for tooltip usage
|
||||
const selectRef = useRef(null);
|
||||
// Check if the select component should be disabled based on `rsssl_settings.email_verified`
|
||||
const isSelectDisabled = ! getFieldValue('login_protection_enabled');
|
||||
|
||||
useEffect(() => {
|
||||
if (!rolesLoaded) {
|
||||
fetchRoles(field.id);
|
||||
}
|
||||
}, [rolesLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if ( field.id === forcedRoledId ) {
|
||||
let otherField = getField(optionalRolesId);
|
||||
let roles = Array.isArray(otherField.value) ? otherField.value : [];
|
||||
setOtherRoles(roles);
|
||||
} else {
|
||||
let otherField = getField(forcedRoledId);
|
||||
let roles = Array.isArray(otherField.value) ? otherField.value : [];
|
||||
setOtherRoles(roles);
|
||||
}
|
||||
}, [selectedRoles, getField(optionalRolesId), getField(forcedRoledId)]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!field.value) {
|
||||
setChangedField(field.id, field.default);
|
||||
updateField(field.id, field.default);
|
||||
setSelectedRoles(field.default.map((role, index) => ({ value: role, label: role.charAt(0).toUpperCase() + role.slice(1) })));
|
||||
} else {
|
||||
setSelectedRoles(field.value.map((role, index) => ({ value: role, label: role.charAt(0).toUpperCase() + role.slice(1) })));
|
||||
}
|
||||
},[fieldsLoaded]);
|
||||
|
||||
const handleChange = (selectedOptions) => {
|
||||
const rolesExcluded = selectedOptions.map(option => option.value);
|
||||
let rolesEnabledEmail = getFieldValue('two_fa_enabled_roles_email');
|
||||
let rolesEnabledTotp = getFieldValue('two_fa_enabled_roles_totp');
|
||||
let rolesEnabled = rolesEnabledEmail.concat(rolesEnabledTotp);
|
||||
|
||||
let rolesEnabledContainsSelected = rolesEnabled.filter(role => selectedOptions.map(option => option.value).includes(role));
|
||||
if (rolesEnabledContainsSelected.length === 0 && selectedOptions.length > 0) {
|
||||
showSavedSettingsNotice(__('You have enforced 2FA, but not configured any methods.', 'really-simple-ssl'), 'error');
|
||||
} else {
|
||||
selectedOptions.forEach(role => {
|
||||
if (!rolesEnabled.includes(role.value)) {
|
||||
showSavedSettingsNotice(__('You have enforced 2FA, but not configured any methods for the role: ', 'really-simple-ssl') + role.label, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedRoles(selectedOptions);
|
||||
updateField(field.id, rolesExcluded);
|
||||
setChangedField(field.id, rolesExcluded);
|
||||
};
|
||||
|
||||
const customStyles = {
|
||||
multiValue: (provided) => ({
|
||||
...provided,
|
||||
borderRadius: '10px',
|
||||
backgroundColor: field.id === forcedRoledId ? '#F5CD54' : field.id === optionalRolesId ? '#FDF5DC' : 'default',
|
||||
}),
|
||||
multiValueRemove: (base, state) => ({
|
||||
...base,
|
||||
color: state.isHovered ? 'initial' : base.color,
|
||||
opacity: '0.7',
|
||||
':hover': {
|
||||
backgroundColor: 'initial',
|
||||
color: 'initial',
|
||||
opacity: '1',
|
||||
},
|
||||
})
|
||||
};
|
||||
|
||||
const alreadySelected = selectedRoles.map(option => option.value);
|
||||
let filteredRoles = [];
|
||||
let inRolesInUse = [...alreadySelected, ...otherRoles];
|
||||
|
||||
roles.forEach(function (item, i) {
|
||||
if (Array.isArray(inRolesInUse) && inRolesInUse.includes(item.value)) {
|
||||
filteredRoles.splice(i, 1);
|
||||
} else {
|
||||
filteredRoles.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{marginTop: '5px'}} ref={selectRef}>
|
||||
<Select
|
||||
isMulti
|
||||
options={filteredRoles}
|
||||
onChange={handleChange}
|
||||
value={selectedRoles}
|
||||
menuPosition={"fixed"}
|
||||
styles={customStyles}
|
||||
isDisabled={isSelectDisabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TwoFaRolesDropDown;
|
||||
@@ -0,0 +1,3 @@
|
||||
div[class$="MenuPortal"] {
|
||||
z-index:30;
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import {useState, useEffect, useRef} from '@wordpress/element';
|
||||
import Icon from "../../utils/Icon";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
TextControl
|
||||
} from "@wordpress/components";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import FieldsData from "../FieldsData";
|
||||
import UserAgentStore from "./UserAgentStore";
|
||||
|
||||
const UserAgentModal = (props) => {
|
||||
const {note, setNote, user_agent, setUserAgent, dataLoaded, addRow, fetchData} = UserAgentStore();
|
||||
const {showSavedSettingsNotice} = FieldsData();
|
||||
const userAgentInputRef = useRef(null);
|
||||
const noteInputRef = useRef(null);
|
||||
|
||||
async function handleSubmit() {
|
||||
// we check if statusSelected is not empty
|
||||
if (user_agent.length) {
|
||||
await addRow(user_agent, note).then((response) => {
|
||||
if (response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
fetchData('rsssl_user_agent_list');
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
});
|
||||
//we clear the input
|
||||
resetValues();
|
||||
//we close the modal
|
||||
props.onRequestClose();
|
||||
}
|
||||
}
|
||||
|
||||
function resetValues() {
|
||||
setUserAgent('');
|
||||
setNote('');
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
resetValues();
|
||||
// Close the modal
|
||||
props.onRequestClose();
|
||||
}
|
||||
|
||||
function handleKeyPress(event) {
|
||||
console.log('i pressed a key' + event.key);
|
||||
if (event.key === 'Enter') {
|
||||
handleSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (userAgentInputRef.current) {
|
||||
userAgentInputRef.current.addEventListener('keypress', handleKeyPress);
|
||||
}
|
||||
if (noteInputRef.current) {
|
||||
noteInputRef.current.addEventListener('keypress', handleKeyPress);
|
||||
}
|
||||
|
||||
// cleanup event listeners
|
||||
return () => {
|
||||
if (userAgentInputRef.current) {
|
||||
userAgentInputRef.current.removeEventListener('keypress', handleKeyPress);
|
||||
}
|
||||
if (noteInputRef.current) {
|
||||
noteInputRef.current.removeEventListener('keypress', handleKeyPress);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!props.isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={__("Block User-Agent", "really-simple-ssl")}
|
||||
shouldCloseOnClickOutside={true}
|
||||
shouldCloseOnEsc={true}
|
||||
overlayClassName="rsssl-modal-overlay"
|
||||
className="rsssl-modal"
|
||||
onRequestClose={props.onRequestClose}
|
||||
onKeyPress={handleKeyPress}
|
||||
>
|
||||
<div className="modal-content">
|
||||
<div className="modal-body"
|
||||
style={{
|
||||
padding: "0.5em",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "95%",
|
||||
height: "100%",
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
<div style={{position: 'relative'}}>
|
||||
<label
|
||||
htmlFor={'user_agent'}
|
||||
className={'rsssl-label'}
|
||||
>{__('User-Agent', 'really-simple-ssl')}</label>
|
||||
<input
|
||||
id={'user_agent'}
|
||||
type={'text'}
|
||||
value={user_agent}
|
||||
name={'user_agent'}
|
||||
onChange={(e) => setUserAgent(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
ref={userAgentInputRef}
|
||||
/>
|
||||
</div>
|
||||
<div style={{marginTop: '10px'}}>
|
||||
<label
|
||||
htmlFor={'note'}
|
||||
className={'rsssl-label'}
|
||||
>{__('Notes', 'really-simple-ssl')}</label>
|
||||
<input
|
||||
name={'note'}
|
||||
id={'note'}
|
||||
type={'text'}
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
ref={noteInputRef}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<div
|
||||
className={'rsssl-grid-item-footer'}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
padding: '1em',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
isSecondary
|
||||
onClick={handleCancel}
|
||||
style={{marginRight: '10px'}}
|
||||
>
|
||||
{__("Cancel", "really-simple-ssl")}
|
||||
</Button>
|
||||
<Button
|
||||
isPrimary
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{__("Add", "really-simple-ssl")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserAgentModal;
|
||||
@@ -0,0 +1,63 @@
|
||||
import { create } from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
|
||||
const UserAgentStore = create((set, get) => ({
|
||||
processing: false,
|
||||
data: [],
|
||||
dataLoaded: false,
|
||||
user_agent: '',
|
||||
note: '',
|
||||
|
||||
fetchData: async (action, filter) => {
|
||||
set({ processing: true });
|
||||
set({ dataLoaded: false });
|
||||
try {
|
||||
const response = await rsssl_api.doAction(action , { filter });
|
||||
if (response.request_success) {
|
||||
set({ data: response.data });
|
||||
if (response.data) {
|
||||
set({ dataLoaded: true });
|
||||
}
|
||||
set({ processing: false });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
set({ dataLoaded: false });
|
||||
} finally {
|
||||
set({ processing: false });
|
||||
}
|
||||
},
|
||||
setNote: (note) => set({ note }),
|
||||
setUserAgent: (user_agent) => set({ user_agent }),
|
||||
setDataLoaded: (dataLoaded) => set({ dataLoaded }),
|
||||
addRow: async (user_agent, note) => {
|
||||
set({ processing: true });
|
||||
try {
|
||||
const response = await rsssl_api.doAction('rsssl_user_agent_add', { user_agent, note });
|
||||
if (response.request_success) {
|
||||
set({ dataLoaded: false });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
set({ processing: false, dataLoaded: true });
|
||||
}
|
||||
return { success: true, message: 'User-Agent added successfully' };
|
||||
},
|
||||
deleteValue: async (id) => {
|
||||
set({ processing: true });
|
||||
try {
|
||||
const response = await rsssl_api.doAction('rsssl_user_agent_delete', { id });
|
||||
if (response.request_success) {
|
||||
set({ dataLoaded: false });
|
||||
return { success: true, message: response.message };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
set({ processing: false, dataLoaded: false });
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
export default UserAgentStore;
|
||||
@@ -0,0 +1,284 @@
|
||||
import { useEffect, useState, useCallback } from '@wordpress/element';
|
||||
import DataTable, { createTheme } from "react-data-table-component";
|
||||
import FieldsData from "../FieldsData";
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import useFields from "../FieldsData";
|
||||
import useMenu from "../../Menu/MenuData";
|
||||
import AddButton from "../GeoBlockList/AddButton";
|
||||
import UserAgentModal from "./UserAgentModal";
|
||||
import UserAgentStore from "./UserAgentStore";
|
||||
import { in_array } from "../../utils/lib";
|
||||
import FilterData from "../FilterData";
|
||||
|
||||
const UserAgentTable = (props) => {
|
||||
const {
|
||||
data,
|
||||
processing,
|
||||
dataLoaded,
|
||||
fetchData,
|
||||
user_agent,
|
||||
note,
|
||||
deleteValue,
|
||||
setDataLoaded,
|
||||
} = UserAgentStore();
|
||||
|
||||
const {
|
||||
selectedFilter,
|
||||
setSelectedFilter,
|
||||
activeGroupId,
|
||||
getCurrentFilter,
|
||||
setProcessingFilter,
|
||||
} = FilterData();
|
||||
|
||||
const moduleName = 'rsssl-group-filter-user_agents';
|
||||
const { fields, fieldAlreadyEnabled, getFieldValue, setHighLightField, getField } = useFields();
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [rowsSelected, setRowsSelected] = useState([]);
|
||||
const [rowCleared, setRowCleared] = useState(false);
|
||||
const [columns, setColumns] = useState([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filteredData, setFilteredData] = useState([]);
|
||||
const { showSavedSettingsNotice, saveFields } = FieldsData();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [filter, setFilter] = useState(getCurrentFilter(moduleName));
|
||||
|
||||
let enabled = getFieldValue('enable_firewall');
|
||||
const IsNull = (value) => value === null;
|
||||
|
||||
useEffect(() => {
|
||||
const currentFilter = getCurrentFilter(moduleName);
|
||||
if (typeof currentFilter === 'undefined') {
|
||||
setFilter('blocked');
|
||||
} else {
|
||||
setFilter(currentFilter);
|
||||
}
|
||||
setRowsSelected([]);
|
||||
}, [getCurrentFilter(moduleName)]);
|
||||
|
||||
useEffect(() => {
|
||||
if (filter !== undefined) {
|
||||
const fetchData = async () => {
|
||||
setDataLoaded(false);
|
||||
}
|
||||
fetchData();
|
||||
setRowsSelected([]);
|
||||
}
|
||||
|
||||
}, [filter]);
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
const handlePerRowsChange = (newRowsPerPage) => {
|
||||
setRowsPerPage(newRowsPerPage);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (props.field) {
|
||||
const buildColumn = (column) => ({
|
||||
name: column.name,
|
||||
sortable: column.sortable,
|
||||
searchable: column.searchable,
|
||||
width: column.width,
|
||||
visible: column.visible,
|
||||
column: column.column,
|
||||
selector: row => row[column.column],
|
||||
});
|
||||
setColumns(props.field.columns.map(buildColumn));
|
||||
}
|
||||
}, [props.field]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserAgentList = async () => {
|
||||
if (!dataLoaded && enabled ) {
|
||||
await fetchData('rsssl_user_agent_list', filter);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserAgentList();
|
||||
}, [dataLoaded, enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
saveFields(false, false, true);
|
||||
setDataLoaded(false);
|
||||
},[enabled]);
|
||||
|
||||
const handleClose = () => {
|
||||
setModalOpen(false);
|
||||
}
|
||||
|
||||
const handleOpen = () => {
|
||||
setModalOpen(true);
|
||||
}
|
||||
|
||||
const ActionButton = ({ onClick, children, className }) => (
|
||||
<button
|
||||
className={`button ${className} rsssl-action-buttons__button`}
|
||||
onClick={onClick}
|
||||
disabled={false}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
const softDelete = useCallback((id) => {
|
||||
if (Array.isArray(id)) {
|
||||
const ids = id.map(item => ({ id: item.id }));
|
||||
deleteValue(ids).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
setRowsSelected([]);
|
||||
});
|
||||
} else {
|
||||
deleteValue(id).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
}
|
||||
}, [deleteValue, rowsSelected, showSavedSettingsNotice]);
|
||||
|
||||
const generateActionButtons = useCallback((id, deleted) => (
|
||||
<div className="rsssl-action-buttons">
|
||||
<ActionButton
|
||||
onClick={() => softDelete(id)}
|
||||
className={deleted ? "button-primary" : "button-red"}
|
||||
>
|
||||
{deleted ? __("Block", "really-simple-ssl") : __("Delete", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</div>
|
||||
), [softDelete]);
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data) { // Add this check to ensure data is not undefined or null
|
||||
const filtered = Object.entries(data)
|
||||
.filter(([_, dataItem]) => {
|
||||
return Object.values(dataItem).some(val => ((val ?? '').toString().toLowerCase().includes(searchTerm.toLowerCase())));
|
||||
})
|
||||
.map(([key, dataItem]) => {
|
||||
const newItem = { ...dataItem,
|
||||
action: generateActionButtons(dataItem.id, !IsNull(dataItem.deleted_at) ) };
|
||||
return [key, newItem];
|
||||
})
|
||||
.reduce((obj, [key, val]) => ({ ...obj, [key]: val }), {});
|
||||
setFilteredData(filtered);
|
||||
}
|
||||
}, [searchTerm, data, generateActionButtons]);
|
||||
|
||||
const handleSelection = useCallback((state) => {
|
||||
//based on the current page and the rows per page we get the rows that are selected
|
||||
const {selectedCount, selectedRows, allSelected, allRowsSelected} = state;
|
||||
let rows = [];
|
||||
if (allSelected) {
|
||||
rows = selectedRows.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage);
|
||||
setRowsSelected(rows);
|
||||
} else {
|
||||
setRowsSelected(selectedRows);
|
||||
}
|
||||
}, [currentPage, rowsPerPage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (rowsSelected.length === 0) {
|
||||
setRowCleared(!rowCleared);
|
||||
}
|
||||
}, [rowsSelected]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserAgentModal
|
||||
isOpen={modalOpen}
|
||||
onRequestClose={handleClose}
|
||||
value={user_agent}
|
||||
status={'blocked'}
|
||||
/>
|
||||
<div className="rsssl-container">
|
||||
<AddButton
|
||||
handleOpen={handleOpen}
|
||||
processing={processing}
|
||||
allowedText={__("Block User-Agent", "really-simple-ssl")}
|
||||
disabled={!dataLoaded}
|
||||
/>
|
||||
<div className="rsssl-search-bar">
|
||||
<div className="rsssl-search-bar__inner">
|
||||
<div className="rsssl-search-bar__icon"></div>
|
||||
<input
|
||||
type="text"
|
||||
className="rsssl-search-bar__input"
|
||||
placeholder={__("Search", "really-simple-ssl")}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{rowsSelected.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
}}
|
||||
>
|
||||
<div className={"rsssl-multiselect-datatable-form rsssl-primary"}>
|
||||
<div>
|
||||
{__("You have selected %s rows", "really-simple-ssl").replace('%s', rowsSelected.length)}
|
||||
</div>
|
||||
<div className="rsssl-action-buttons">
|
||||
<ActionButton
|
||||
onClick={() => softDelete(rowsSelected)}
|
||||
className="button-red"
|
||||
>
|
||||
{__("Delete", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={Object.values(filteredData)}
|
||||
pagination={true}
|
||||
paginationComponentOptions={{
|
||||
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
|
||||
rangeSeparatorText: __('of', 'really-simple-ssl'),
|
||||
noRowsPerPage: false,
|
||||
selectAllRowsItem: false,
|
||||
selectAllRowsItemText: __('All', 'really-simple-ssl'),
|
||||
}}
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
onSelectedRowsChange={handleSelection}
|
||||
clearSelectedRows={rowCleared}
|
||||
paginationPerPage={rowsPerPage}
|
||||
onChangePage={handlePageChange}
|
||||
onChangeRowsPerPage={handlePerRowsChange}
|
||||
theme="really-simple-plugins"
|
||||
customStyles={customStyles}
|
||||
selectableRows={true}
|
||||
/>
|
||||
{!getFieldValue('enable_firewall') && (
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay">
|
||||
<span className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span>
|
||||
<span>
|
||||
{__('Restrict access from specific countries or continents. You can also allow only specific countries.', 'really-simple-ssl')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserAgentTable;
|
||||
Reference in New Issue
Block a user