Initial commit: Atomaste website

This commit is contained in:
2025-12-10 12:17:30 -05:00
commit 0b9e5d1605
19260 changed files with 5206382 additions and 0 deletions

View File

@@ -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;
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 */
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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],
};
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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)}&nbsp;
<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}&nbsp;
<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}&nbsp;
<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")}&nbsp;
<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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
})
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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")}&nbsp;</>}
{ !props.disabled && <>{__("Permissions Policy is enforced.", "really-simple-ssl")}&nbsp;</>}
{ !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

View File

@@ -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;

View File

@@ -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}&nbsp;</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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -0,0 +1,5 @@
.rsssl-modal.rsssl-vulnerabilities-modal{
ul {
column-count: 1;
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,3 @@
div[class$="MenuPortal"] {
z-index:30;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;