Initial commit: Atomaste website
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import GridBlock from "./GridBlock";
|
||||
import ProgressHeader from "./Progress/ProgressBlockHeader";
|
||||
import ProgressBlock from "./Progress/ProgressBlock";
|
||||
import ProgressFooter from "./Progress/ProgressFooter";
|
||||
import SslLabsHeader from "./SslLabs/SslLabsHeader";
|
||||
import SslLabs from "./SslLabs/SslLabs";
|
||||
import SslLabsFooter from "./SslLabs/SslLabsFooter";
|
||||
import VulnerabilitiesHeader from "./Vulnerabilities/VulnerabilitiesHeader";
|
||||
import Vulnerabilities from "./Vulnerabilities/Vulnerabilities";
|
||||
import VulnerabilitiesFooter from "./Vulnerabilities/VulnerabilitiesFooter";
|
||||
import TipsTricks from "./TipsTricks/TipsTricks";
|
||||
import TipsTricksFooter from "./TipsTricks/TipsTricksFooter";
|
||||
import OtherPluginsHeader from "./OtherPlugins/OtherPluginsHeader";
|
||||
import OtherPlugins from "./OtherPlugins/OtherPlugins";
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import DashboardPlaceholder from "../Placeholder/DashboardPlaceholder";
|
||||
import useFields from "../Settings/FieldsData";
|
||||
import ErrorBoundary from "../utils/ErrorBoundary";
|
||||
|
||||
const DashboardPage = () => {
|
||||
const {fieldsLoaded} = useFields();
|
||||
|
||||
const blocks = [
|
||||
{
|
||||
id: 'progress',
|
||||
header: ProgressHeader,
|
||||
content: ProgressBlock,
|
||||
footer: ProgressFooter,
|
||||
class: ' rsssl-column-2',
|
||||
},
|
||||
{
|
||||
id: 'ssllabs',
|
||||
header: SslLabsHeader,
|
||||
content: SslLabs,
|
||||
footer: SslLabsFooter,
|
||||
class: 'border-to-border',
|
||||
},
|
||||
{
|
||||
id: 'wpvul',
|
||||
header: VulnerabilitiesHeader,
|
||||
content: Vulnerabilities,
|
||||
footer: VulnerabilitiesFooter,
|
||||
class: 'border-to-border',
|
||||
},
|
||||
{
|
||||
id: 'tips_tricks',
|
||||
title: __("Tips & Tricks", 'really-simple-ssl'),
|
||||
content: TipsTricks,
|
||||
footer: TipsTricksFooter,
|
||||
class: ' rsssl-column-2',
|
||||
},
|
||||
{
|
||||
id: 'other-plugins',
|
||||
header: OtherPluginsHeader,
|
||||
content: OtherPlugins,
|
||||
class: ' rsssl-column-2 no-border no-background',
|
||||
},
|
||||
]
|
||||
return (
|
||||
<>
|
||||
{!fieldsLoaded && <DashboardPlaceholder></DashboardPlaceholder>}
|
||||
{fieldsLoaded && blocks.map((block, i) => <ErrorBoundary key={"grid_"+i} fallback={"Could not load dashboard block"}><GridBlock block={block}/></ErrorBoundary>)}
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
export default DashboardPage
|
||||
@@ -0,0 +1,22 @@
|
||||
const GridBlock = (props) => {
|
||||
const footer =props.block.footer ? props.block.footer : false;
|
||||
const blockData = props.block;
|
||||
let className = "rsssl-grid-item "+blockData.class+" rsssl-"+blockData.id;
|
||||
return (
|
||||
<div key={"block-"+blockData.id} className={className}>
|
||||
<div key={"header-"+blockData.id} className="rsssl-grid-item-header">
|
||||
{ blockData.header && wp.element.createElement(blockData.header) }
|
||||
{ !blockData.header && <>
|
||||
<h3 className="rsssl-grid-title rsssl-h4">{ blockData.title }</h3>
|
||||
<div className="rsssl-grid-item-controls"></div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
<div key={"content-"+blockData.id} className="rsssl-grid-item-content">{wp.element.createElement(props.block.content)}</div>
|
||||
{ !footer && <div key={"footer-"+blockData.id} className="rsssl-grid-item-footer"></div>}
|
||||
{ footer && <div key={"footer-"+blockData.id} className="rsssl-grid-item-footer">{wp.element.createElement(footer)}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GridBlock;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useEffect} from "@wordpress/element";
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import Placeholder from '../../Placeholder/Placeholder';
|
||||
import useOtherPlugins from "./OtherPluginsData";
|
||||
const OtherPlugins = () => {
|
||||
const {dataLoaded, pluginData, pluginActions, fetchOtherPluginsData, error} = useOtherPlugins();
|
||||
useEffect(() => {
|
||||
if (!dataLoaded) {
|
||||
fetchOtherPluginsData();
|
||||
}
|
||||
}, [] )
|
||||
|
||||
const otherPluginElement = (plugin, i) => {
|
||||
return (
|
||||
<div key={"plugin"+i} className={"rsssl-other-plugins-element rsssl-"+plugin.slug}>
|
||||
<a href={plugin.wordpress_url} target="_blank" rel="noopener noreferrer" title={plugin.title}>
|
||||
<div className="rsssl-bullet"></div>
|
||||
<div className="rsssl-other-plugins-content">{plugin.title}</div>
|
||||
</a>
|
||||
<div className="rsssl-other-plugin-status">
|
||||
{plugin.pluginAction==='upgrade-to-premium' && <><a target="_blank" rel="noopener noreferrer" href={plugin.upgrade_url}>{__("Upgrade", "really-simple-ssl")}</a></>}
|
||||
{plugin.pluginAction!=='upgrade-to-premium' && plugin.pluginAction!=='installed' && <>
|
||||
<a href="#" onClick={ (e) => pluginActions(plugin.slug, plugin.pluginAction, e) } >{plugin.pluginActionNice}</a></>}
|
||||
{plugin.pluginAction==='installed' && <>{__("Installed", "really-simple-ssl")}</>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if ( !dataLoaded || error) {
|
||||
return (<Placeholder lines="3" error={error}></Placeholder>)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-other-plugins-container">
|
||||
{ pluginData.map((plugin, i) => otherPluginElement(plugin, i)) }
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default OtherPlugins;
|
||||
@@ -0,0 +1,81 @@
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import {__} from "@wordpress/i18n";
|
||||
const useOtherPlugins = create(( set, get ) => ({
|
||||
error:false,
|
||||
dataLoaded:false,
|
||||
pluginData:[],
|
||||
updatePluginData:(slug, newPluginItem) => {
|
||||
let pluginData = get().pluginData;
|
||||
pluginData.forEach(function(pluginItem, i) {
|
||||
if (pluginItem.slug===slug) {
|
||||
pluginData[i] = newPluginItem;
|
||||
}
|
||||
});
|
||||
set(state => ({
|
||||
dataLoaded:true,
|
||||
pluginData:pluginData,
|
||||
}))
|
||||
},
|
||||
getPluginData: (slug) => {
|
||||
let pluginData = get().pluginData;
|
||||
return pluginData.filter((pluginItem) => {
|
||||
return (pluginItem.slug===slug)
|
||||
})[0];
|
||||
},
|
||||
fetchOtherPluginsData: async () => {
|
||||
const {pluginData, error} = await rsssl_api.doAction('otherpluginsdata').then((response) => {
|
||||
let pluginData = [];
|
||||
pluginData = response.plugins;
|
||||
let error = response.error;
|
||||
if (!error) {
|
||||
pluginData.forEach(function (pluginItem, i) {
|
||||
pluginData[i].pluginActionNice = pluginActionNice(pluginItem.pluginAction);
|
||||
});
|
||||
}
|
||||
|
||||
return {pluginData, error};
|
||||
})
|
||||
set(state => ({
|
||||
dataLoaded:true,
|
||||
pluginData:pluginData,
|
||||
error:error,
|
||||
}))
|
||||
},
|
||||
pluginActions: (slug, pluginAction, e) => {
|
||||
if (e) e.preventDefault();
|
||||
let data = {};
|
||||
data.slug = slug;
|
||||
data.pluginAction = pluginAction;
|
||||
let pluginItem = get().getPluginData(slug);
|
||||
if ( pluginAction==='download' ) {
|
||||
pluginItem.pluginAction = "downloading";
|
||||
} else if (pluginAction==='activate') {
|
||||
pluginItem.pluginAction = "activating";
|
||||
}
|
||||
pluginItem.pluginActionNice = pluginActionNice(pluginItem.pluginAction);
|
||||
get().updatePluginData(slug, pluginItem);
|
||||
if (pluginAction==='installed' || pluginAction === 'upgrade-to-premium') {
|
||||
return;
|
||||
}
|
||||
rsssl_api.doAction('plugin_actions', data).then( ( response ) => {
|
||||
pluginItem = response;
|
||||
get().updatePluginData(slug, pluginItem);
|
||||
get().pluginActions(slug, pluginItem.pluginAction);
|
||||
})
|
||||
},
|
||||
}));
|
||||
|
||||
export default useOtherPlugins;
|
||||
|
||||
const pluginActionNice = (pluginAction) => {
|
||||
const statuses = {
|
||||
'download': __("Install", "really-simple-ssl"),
|
||||
'activate': __("Activate", "really-simple-ssl"),
|
||||
'activating': __("Activating...", "really-simple-ssl"),
|
||||
'downloading': __("Downloading...", "really-simple-ssl"),
|
||||
'upgrade-to-premium': __("Downloading...", "really-simple-ssl"),
|
||||
};
|
||||
return statuses[pluginAction];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
const OtherPluginsHeader = () => {
|
||||
return (
|
||||
<>
|
||||
<h3 className="rsssl-grid-title rsssl-h4">{ __( "Other Plugins", 'really-simple-ssl' ) }</h3>
|
||||
<div className="rsssl-grid-item-controls">
|
||||
<span className="rsssl-header-html">
|
||||
<a className="rsp-logo" href="https://really-simple-plugins.com/"><img src={rsssl_settings.plugin_url+"assets/img/really-simple-plugins.svg"} alt="Really Simple Plugins"/></a>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default OtherPluginsHeader;
|
||||
@@ -0,0 +1,125 @@
|
||||
import {
|
||||
useState, useEffect, useRef
|
||||
} from '@wordpress/element';
|
||||
import TaskElement from "./../TaskElement";
|
||||
import useProgress from "./ProgressData";
|
||||
import {__} from "@wordpress/i18n";
|
||||
|
||||
const ProgressBlock = (props) => {
|
||||
const {percentageCompleted, progressText, filter, notices, progressLoaded, getProgressData, error} = useProgress();
|
||||
useEffect( () => {
|
||||
getProgressData();
|
||||
|
||||
}, [] );
|
||||
|
||||
const getStyles = () => {
|
||||
return Object.assign(
|
||||
{},
|
||||
{width: percentageCompleted+"%"},
|
||||
);
|
||||
}
|
||||
|
||||
let progressBarColor = '';
|
||||
if ( percentageCompleted<80 ) {
|
||||
progressBarColor += 'rsssl-orange';
|
||||
}
|
||||
|
||||
if ( !progressLoaded || error ) {
|
||||
return (
|
||||
<div className="rsssl-progress-block">
|
||||
<div className="rsssl-progress-bar">
|
||||
<div className="rsssl-progress">
|
||||
<div className={'rsssl-bar rsssl-orange'} style={getStyles()}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rsssl-progress-text">
|
||||
<h1 className="rsssl-progress-percentage">
|
||||
0%
|
||||
</h1>
|
||||
<h5 className="rsssl-progress-text-span">
|
||||
{__('Loading...', 'really-simple-ssl')}
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div className="rsssl-scroll-container">
|
||||
<div className="rsssl-task-element">
|
||||
<span className={'rsssl-task-status rsssl-loading'}>{__('Loading...', 'really-simple-ssl')}</span>
|
||||
<p className="rsssl-task-message">{__('Loading...', 'really-simple-ssl')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let noticesOutput = notices;
|
||||
if ( filter==='remaining' ) {
|
||||
noticesOutput = noticesOutput.filter(function (notice) {
|
||||
return notice.output.status==='open' || notice.output.status==='warning';
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rsssl-progress-block">
|
||||
<div className="rsssl-progress-bar">
|
||||
<div className="rsssl-progress">
|
||||
<div className={'rsssl-bar ' + progressBarColor} style={getStyles()}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rsssl-progress-text">
|
||||
<AnimatedPercentage percentageCompleted={percentageCompleted} />
|
||||
<h5 className="rsssl-progress-text-span">
|
||||
{progressText}
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div className="rsssl-scroll-container">
|
||||
{noticesOutput.map((notice, i) => <TaskElement key={"task-"+i} notice={notice}/>)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
export default ProgressBlock;
|
||||
|
||||
export const AnimatedPercentage = ({ percentageCompleted }) => {
|
||||
const [displayedPercentage, setDisplayedPercentage] = useState(0);
|
||||
// useRef previous percentageCompleted
|
||||
const prevPercentageCompleted = useRef(0);
|
||||
const easeOutCubic = (t) => {
|
||||
return 1 - Math.pow(1 - t, 3);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const startPercentage = prevPercentageCompleted.current;
|
||||
const animationDuration = 1000;
|
||||
const startTime = Date.now();
|
||||
|
||||
const animatePercentage = () => {
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
const progress = Math.min(elapsedTime / animationDuration, 1);
|
||||
const easedProgress = easeOutCubic(progress);
|
||||
|
||||
const newPercentage = Math.min(startPercentage + (percentageCompleted - startPercentage) * easedProgress, percentageCompleted);
|
||||
|
||||
if (progress < 1) {
|
||||
// update displayedPercentage
|
||||
setDisplayedPercentage(newPercentage);
|
||||
prevPercentageCompleted.current = percentageCompleted;
|
||||
} else {
|
||||
// update prevPercentageCompleted to the new percentageCompleted
|
||||
clearInterval(animationInterval);
|
||||
}
|
||||
};
|
||||
|
||||
const animationInterval = setInterval(animatePercentage, 16);
|
||||
return () => clearInterval(animationInterval);
|
||||
}, [percentageCompleted]);
|
||||
|
||||
return (
|
||||
<h1 className="rsssl-progress-percentage">
|
||||
{Math.round(displayedPercentage)}%
|
||||
</h1>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
useEffect,
|
||||
} from '@wordpress/element';
|
||||
import useProgress from "./ProgressData";
|
||||
|
||||
const ProgressHeader = () => {
|
||||
const {setFilter, filter, fetchFilter, notices, error } = useProgress();
|
||||
|
||||
useEffect( () => {
|
||||
fetchFilter();
|
||||
}, [] );
|
||||
|
||||
const onClickHandler = (e) => {
|
||||
let filter = e.target.getAttribute('data-filter');
|
||||
if (filter==='all' || filter==='remaining') {
|
||||
setFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
if (error ) {
|
||||
return (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
let all_task_count = 0;
|
||||
let open_task_count = 0;
|
||||
all_task_count = notices.length;
|
||||
let openNotices = notices.filter(function (notice) {
|
||||
return notice.output.status==='open' || notice.output.status==='warning';
|
||||
});
|
||||
open_task_count = openNotices.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="rsssl-grid-title rsssl-h4">{ __( "Progress", 'really-simple-ssl' ) }</h3>
|
||||
<div className="rsssl-grid-item-controls">
|
||||
<div className={"rsssl-task-switcher-container rsssl-active-filter-"+filter}>
|
||||
<span className="rsssl-task-switcher rsssl-all-tasks" onClick={onClickHandler} htmlFor="rsssl-all-tasks" data-filter="all">
|
||||
{ __( "All tasks", "really-simple-ssl" ) }
|
||||
<span className="rsssl_task_count">({all_task_count})</span>
|
||||
</span>
|
||||
<span className="rsssl-task-switcher rsssl-remaining-tasks" onClick={onClickHandler} htmlFor="rsssl-remaining-tasks" data-filter="remaining">
|
||||
{ __( "Remaining tasks", "really-simple-ssl" )}
|
||||
<span className="rsssl_task_count">({open_task_count})</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
export default ProgressHeader;
|
||||
@@ -0,0 +1,47 @@
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
const useProgress = create(( set, get ) => ({
|
||||
filter:'all',
|
||||
progressText:'',
|
||||
notices: [],
|
||||
error:false,
|
||||
percentageCompleted:0,
|
||||
progressLoaded:false,
|
||||
setFilter: (filter) => {
|
||||
sessionStorage.rsssl_task_filter = filter;
|
||||
|
||||
set(state => ({ filter }))
|
||||
},
|
||||
fetchFilter: () => {
|
||||
if ( typeof (Storage) !== "undefined" && sessionStorage.rsssl_task_filter ) {
|
||||
let filter = sessionStorage.rsssl_task_filter;
|
||||
set(state => ({ filter:filter }))
|
||||
|
||||
}
|
||||
},
|
||||
getProgressData: async () => {
|
||||
const {percentage, text, notices, error } = await rsssl_api.runTest('progressData', 'refresh').then( ( response ) => {
|
||||
return response;
|
||||
});
|
||||
|
||||
set(state => ({
|
||||
notices:notices,
|
||||
percentageCompleted:percentage,
|
||||
progressText:text,
|
||||
progressLoaded:true,
|
||||
error:error,
|
||||
}))
|
||||
},
|
||||
dismissNotice: async (noticeId) => {
|
||||
let notices = get().notices;
|
||||
notices = notices.filter(function (notice) {
|
||||
return notice.id !== noticeId;
|
||||
});
|
||||
set(state => ({ notices:notices }))
|
||||
const {percentage} = await rsssl_api.runTest('dismiss_task', noticeId);
|
||||
set({ percentageCompleted:percentage })
|
||||
}
|
||||
}));
|
||||
|
||||
export default useProgress;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import Icon from "../../utils/Icon";
|
||||
import useFields from "../../Settings/FieldsData";
|
||||
import useOnboardingData from "../../Onboarding/OnboardingData";
|
||||
|
||||
const ProgressFooter = (props) => {
|
||||
const {setShowOnBoardingModal} = useOnboardingData();
|
||||
const {fields} = useFields();
|
||||
|
||||
let vulnerabilityScanValue = fields.filter( field => field.id==='enable_vulnerability_scanner' )[0].value;
|
||||
let sslEnabled = fields.filter( field => field.id==='ssl_enabled' )[0].value;
|
||||
let wpconfigFixRequired = rsssl_settings.wpconfig_fix_required;
|
||||
let firewallEnabled = fields.filter( field => field.id==='enable_firewall' )[0].value;
|
||||
let sslStatusText = sslEnabled ? __( "SSL", "really-simple-ssl" ) : __( "SSL", "really-simple-ssl" );
|
||||
let sslStatusIcon = sslEnabled ? 'circle-check' : 'circle-times';
|
||||
let sslStatusColor = sslEnabled ? 'green' : 'red';
|
||||
let vulnerabilityIcon = vulnerabilityScanValue ? 'circle-check' : 'circle-times';
|
||||
let vulnerabilityColor = vulnerabilityScanValue ? 'green' : 'red';
|
||||
let firewallIcon = firewallEnabled ? 'circle-check' : 'circle-times';
|
||||
let firewallColor = firewallEnabled ? 'green' : 'red';
|
||||
return (
|
||||
<>
|
||||
{!sslEnabled && <button disabled={wpconfigFixRequired} onClick={() => setShowOnBoardingModal(true)}
|
||||
className="button button-primary">{__("Activate SSL", "really-simple-ssl")}</button>}
|
||||
{rsssl_settings.pro_plugin_active &&
|
||||
<span className="rsssl-footer-left">Really Simple Security Pro {rsssl_settings.pro_version}</span>}
|
||||
{!rsssl_settings.pro_plugin_active &&
|
||||
<a href={rsssl_settings.upgrade_link} target="_blank" rel="noopener noreferrer"
|
||||
className="button button-default">{__("Go Pro", "really-simple-ssl")}</a>}
|
||||
|
||||
<div className="rsssl-legend">
|
||||
<Icon name={sslStatusIcon} color={sslStatusColor}/>
|
||||
<div className={"rsssl-progress-footer-link"}>
|
||||
<a href="#settings/encryption">
|
||||
{sslStatusText}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rsssl-legend">
|
||||
<Icon name={firewallIcon} color={firewallColor}/>
|
||||
<div className={"rsssl-progress-footer-link"}>
|
||||
{firewallEnabled ? (
|
||||
<a href="#settings/rules">
|
||||
{__("Firewall", "really-simple-ssl")}
|
||||
</a>
|
||||
) : (
|
||||
<a href="#settings/firewall&highlightfield=enable_firewall">
|
||||
{__("Firewall", "really-simple-ssl")}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rsssl-legend">
|
||||
<Icon name={vulnerabilityIcon} color={vulnerabilityColor}/>
|
||||
<div className={"rsssl-progress-footer-link"}>
|
||||
{vulnerabilityScanValue ? (
|
||||
<a href="#settings/vulnerabilities">
|
||||
{__("Vulnerability scan", "really-simple-ssl")}
|
||||
</a>
|
||||
) : (
|
||||
<a href="#settings/vulnerabilities&highlightfield=enable_vulnerability_scanner">
|
||||
{__("Vulnerability scan", "really-simple-ssl")}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProgressFooter;
|
||||
@@ -0,0 +1,341 @@
|
||||
import { useEffect, useState, useRef} from "@wordpress/element";
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import Icon from "../../utils/Icon";
|
||||
import useSslLabs from "./SslLabsData";
|
||||
import {getRelativeTime} from "../../utils/formatting";
|
||||
import {addUrlRef} from "../../utils/AddUrlRef";
|
||||
const ScoreElement = ({className, content, id}) => {
|
||||
const [hover, setHover] = useState(false);
|
||||
|
||||
let hoverClass = hover ? 'rsssl-hover' : '';
|
||||
return (
|
||||
<div key={'score_container-'+id} className="rsssl-score-container"><div
|
||||
onMouseEnter={()=> setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
className={"rsssl-score-snippet "+className+' '+hoverClass}>{content}</div></div>
|
||||
)
|
||||
}
|
||||
|
||||
const SslLabs = () => {
|
||||
const {
|
||||
dataLoaded,
|
||||
clearCache,
|
||||
endpointData,
|
||||
setEndpointData,
|
||||
sslData,
|
||||
setSslData,
|
||||
sslScanStatus,
|
||||
setSslScanStatus,
|
||||
isLocalHost,
|
||||
fetchSslData,
|
||||
runSslTest,
|
||||
intervalId,
|
||||
setIntervalId,
|
||||
requestActive,
|
||||
setRequestActive,
|
||||
setClearCache
|
||||
} = useSslLabs();
|
||||
const hasRunOnce = useRef(false);
|
||||
|
||||
|
||||
useEffect(()=>{
|
||||
if ( !dataLoaded ) {
|
||||
fetchSslData();
|
||||
}
|
||||
} , [])
|
||||
|
||||
const neverScannedYet = () => {
|
||||
return !sslData;
|
||||
}
|
||||
|
||||
useEffect(()=> {
|
||||
if ( isLocalHost() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sslScanStatus==='active' && sslData.summary && sslData.summary.progress>=100 ) {
|
||||
setClearCache(true);
|
||||
hasRunOnce.current = false;
|
||||
setSslData(false);
|
||||
setEndpointData(false);
|
||||
}
|
||||
|
||||
if (sslScanStatus==='active' && sslData.status === 'ERROR' ) {
|
||||
setClearCache(true);
|
||||
setSslData(false);
|
||||
setEndpointData(false);
|
||||
}
|
||||
|
||||
let scanInComplete = (sslData && sslData.status !== 'READY');
|
||||
let userClickedStartScan = sslScanStatus==='active';
|
||||
if (clearCache) scanInComplete = true;
|
||||
let hasErrors = sslData.errors || sslData.status === 'ERROR';
|
||||
let startScan = !hasErrors && (scanInComplete || userClickedStartScan);
|
||||
if ( !requestActive && startScan ) {
|
||||
setSslScanStatus('active');
|
||||
setRequestActive(true);
|
||||
if ( !hasRunOnce.current ) {
|
||||
runSslTest();
|
||||
if (!intervalId) {
|
||||
let newIntervalId = setInterval(function () {
|
||||
runSslTest();
|
||||
}, 4000);
|
||||
setIntervalId(newIntervalId);
|
||||
}
|
||||
hasRunOnce.current = true;
|
||||
}
|
||||
} else if ( sslData && sslData.status === 'READY' ) {
|
||||
setSslScanStatus('completed');
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
}, [sslScanStatus, sslData]);
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get some styles for the progress bar
|
||||
* @returns {{width: string}}
|
||||
*/
|
||||
const getStyles = () => {
|
||||
let progress = 0;
|
||||
if (sslData && sslData.summary.progress) {
|
||||
progress = sslData.summary.progress;
|
||||
} else if (progress==0 && sslScanStatus ==='active') {
|
||||
progress=5;
|
||||
}
|
||||
|
||||
return Object.assign(
|
||||
{},
|
||||
{width: progress+"%"},
|
||||
);
|
||||
}
|
||||
|
||||
const scoreSnippet = (className, content, id) => {
|
||||
return (
|
||||
<ScoreElement className={className} content={content} id={id}/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve information from SSL labs if HSTS is detected
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const hasHSTS = () => {
|
||||
let status = 'processing';
|
||||
if ( neverScannedYet() ){
|
||||
status = 'inactive';
|
||||
}
|
||||
|
||||
if ( endpointData && endpointData.length>0 ) {
|
||||
let failedData = endpointData.filter(function (endpoint) {
|
||||
return endpoint.details.hstsPolicy && endpoint.details.hstsPolicy.status!=='present';
|
||||
});
|
||||
status = failedData.length>0 ? 'error' : 'success';
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{(status==='inactive') && scoreSnippet("rsssl-test-inactive", "HSTS",'hsts')}
|
||||
{status==='processing' && scoreSnippet("rsssl-test-processing", "HSTS...", 'hsts')}
|
||||
{status==='error' && scoreSnippet("rsssl-test-error", "No HSTS header", 'hsts')}
|
||||
{status==='success' && scoreSnippet("rsssl-test-success", "HSTS header detected", 'hsts')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cipher strength
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const cipherStrength = () => {
|
||||
// Start with the score of the strongest cipher.
|
||||
// Add the score of the weakest cipher.
|
||||
// Divide the total by 2.
|
||||
let rating = 0;
|
||||
let ratingClass = 'rsssl-test-processing';
|
||||
if ( neverScannedYet() ){
|
||||
ratingClass = 'rsssl-test-inactive';
|
||||
}
|
||||
if ( endpointData && endpointData.length>0 ) {
|
||||
let weakest = 256;
|
||||
let strongest = 128;
|
||||
endpointData.forEach(function(endpoint, i){
|
||||
endpoint.details.suites && endpoint.details.suites.forEach(function(suite, j){
|
||||
suite.list.forEach(function(cipher, j){
|
||||
weakest = cipher.cipherStrength<weakest ? cipher.cipherStrength : weakest;
|
||||
strongest = cipher.cipherStrength>strongest ? cipher.cipherStrength : strongest;
|
||||
});
|
||||
});
|
||||
});
|
||||
rating = (getCypherRating(weakest) + getCypherRating(strongest) )/2;
|
||||
rating = Math.round(rating);
|
||||
ratingClass = rating>70 ? "rsssl-test-success" : "rsssl-test-error";
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{scoreSnippet(ratingClass, __("Cipher strength","really-simple-ssl")+' '+rating+'%','cipher')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* https://github.com/ssllabs/research/wiki/SSL-Server-Rating-Guide#Certificate-strength
|
||||
*/
|
||||
const getCypherRating = (strength) => {
|
||||
let score = 0;
|
||||
if (strength==0) {
|
||||
score = 0;
|
||||
} else if (strength<128){
|
||||
score = 20;
|
||||
} else if (strength<256){
|
||||
score=80;
|
||||
} else {
|
||||
score=100;
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
const certificateStatus = () => {
|
||||
let status = 'processing';
|
||||
if ( neverScannedYet() ){
|
||||
status = 'inactive';
|
||||
}
|
||||
if ( endpointData && endpointData.length>0 ) {
|
||||
let failedData = endpointData.filter(function (endpoint) {
|
||||
return endpoint.grade && endpoint.grade.indexOf('A')===-1;
|
||||
});
|
||||
status = failedData.length>0 ? 'error' : 'success';
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{(status==='inactive') && scoreSnippet("rsssl-test-inactive", "Certificate", "certificate")}
|
||||
{status==='processing' && scoreSnippet("rsssl-test-processing", "Certificate...", "certificate")}
|
||||
{status==='error' && !hasErrors && scoreSnippet("rsssl-test-error", "Certificate issue", "certificate")}
|
||||
{status==='success' && scoreSnippet("rsssl-test-success", "Valid certificate", "certificate")}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const supportsTlS11 = () => {
|
||||
let status = 'processing';
|
||||
if ( neverScannedYet() ){
|
||||
status = 'inactive';
|
||||
}
|
||||
if ( endpointData && endpointData.length>0 ) {
|
||||
status = 'success';
|
||||
endpointData.forEach(function(endpoint, i){
|
||||
endpoint.details.protocols && endpoint.details.protocols.forEach(function(protocol, j){
|
||||
if (protocol.version==='1.1') status = 'error';
|
||||
});
|
||||
});
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{(status==='inactive') && scoreSnippet("rsssl-test-inactive", "Protocol support", "protocol")}
|
||||
{(status==='processing') && scoreSnippet("rsssl-test-processing", "Protocol support...", "protocol")}
|
||||
{status==='error' && scoreSnippet("rsssl-test-error", "Supports TLS 1.1", "protocol")}
|
||||
{status==='success' && scoreSnippet("rsssl-test-success", "No TLS 1.1", "protocol")}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
let sslClass = 'rsssl-inactive';
|
||||
let progress = sslData ? sslData.summary.progress : 0;
|
||||
let startTime = sslData ? sslData.summary.startTime : false;
|
||||
let startTimeNice='';
|
||||
if ( startTime ) {
|
||||
let newDate = new Date();
|
||||
newDate.setTime(startTime);
|
||||
startTimeNice = getRelativeTime(startTime);
|
||||
} else {
|
||||
startTimeNice = __("No test started yet","really-simple-ssl")
|
||||
}
|
||||
|
||||
let statusMessage = sslData ? sslData.summary.statusMessage : false;
|
||||
let grade = sslData ? sslData.summary.grade : '?';
|
||||
if ( sslData && sslData.status === 'READY' ) {
|
||||
if ( grade.indexOf('A')!==-1 ){
|
||||
sslClass = "rsssl-success";
|
||||
} else {
|
||||
sslClass = "rsssl-error";
|
||||
}
|
||||
}
|
||||
|
||||
if (neverScannedYet()){
|
||||
sslClass = "rsssl-inactive";
|
||||
}
|
||||
|
||||
let gradeClass = neverScannedYet() ? 'inactive' : grade;
|
||||
let url = 'https://www.ssllabs.com/analyze.html?d='+encodeURIComponent(window.location.protocol + "//" + window.location.host);
|
||||
let hasErrors = false;
|
||||
let errorMessage='';
|
||||
let sslStatusColor = 'black';
|
||||
|
||||
if ( isLocalHost() ) {
|
||||
hasErrors = true;
|
||||
sslStatusColor = 'red';
|
||||
errorMessage = __("Not available on localhost","really-simple-ssl");
|
||||
} else if (sslData && (sslData.errors || sslData.status === 'ERROR') ) {
|
||||
hasErrors = true;
|
||||
sslStatusColor = 'red';
|
||||
errorMessage = statusMessage;
|
||||
} else if (sslData && progress<100 ) {
|
||||
hasErrors = true;
|
||||
sslStatusColor = 'orange';
|
||||
errorMessage = statusMessage;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'rsssl-ssl-labs'}>
|
||||
<div className={"rsssl-gridblock-progress-container "+sslClass}>
|
||||
<div className="rsssl-gridblock-progress" style={getStyles()}></div>
|
||||
</div>
|
||||
<div className="rsssl-gridblock-progress"
|
||||
style={getStyles()}></div>
|
||||
<div className={"rsssl-ssl-labs-select " + sslClass}>
|
||||
<div className="rsssl-ssl-labs-select-item">
|
||||
{supportsTlS11()}
|
||||
{hasHSTS()}
|
||||
{certificateStatus()}
|
||||
{cipherStrength()}
|
||||
</div>
|
||||
<div className="rsssl-ssl-labs-select-item">
|
||||
{!neverScannedYet() ? <h2 className={'big-number'}>{grade}</h2> : <h2 className={'big-number'}>?</h2>}
|
||||
{neverScannedYet() && <div></div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rsssl-ssl-labs-list">
|
||||
<div className="rsssl-ssl-labs-list-item">
|
||||
<Icon name="info" color={sslStatusColor}/>
|
||||
<p className="rsssl-ssl-labs-list-item-text">
|
||||
{hasErrors && errorMessage}
|
||||
{!hasErrors && __('What does my score mean?', 'really-simple-ssl')}
|
||||
</p>
|
||||
<a href={addUrlRef("https://really-simple-ssl.com/instructions/about-ssl-labs/")} target="_blank" rel="noopener noreferrer">
|
||||
{__('Read more', 'really-simple-ssl')}
|
||||
</a>
|
||||
</div>
|
||||
<div className="rsssl-ssl-labs-list-item">
|
||||
<Icon name="list" color="black"/>
|
||||
<p className="rsssl-ssl-labs-list-item-text">
|
||||
{__('Last check:',
|
||||
'really-simple-ssl')}
|
||||
</p>
|
||||
<p className="rsssl-ssl-labs-list-item-text">{startTimeNice}</p>
|
||||
</div>
|
||||
{ <div className="rsssl-ssl-labs-list-item">
|
||||
<Icon name="external-link" color="black"/>
|
||||
<a href={url} target="_blank" rel="noopener noreferrer">{__('View detailed report on Qualys SSL Labs', 'really-simple-ssl')}</a>
|
||||
</div> }
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SslLabs;
|
||||
@@ -0,0 +1,169 @@
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
const useSslLabs = create(( set, get ) => ({
|
||||
debug:false, //set to true for localhost testing, with wordpress.org as domain
|
||||
sslScanStatus: false,
|
||||
sslData: false,
|
||||
endpointData: [],
|
||||
dataLoaded:false,
|
||||
clearCache:false,
|
||||
requestActive:false,
|
||||
intervalId:false,
|
||||
setIntervalId: (intervalId) => set({ intervalId }),
|
||||
setRequestActive: (requestActive) => set({ requestActive }),
|
||||
setSslScanStatus: (sslScanStatus) => set({ sslScanStatus }),
|
||||
setClearCache: (clearCache) => set({ clearCache }),
|
||||
setSslData: (sslData) => set({ sslData }),
|
||||
setEndpointData: (endpointData) => set({ endpointData }),
|
||||
|
||||
isLocalHost: () => {
|
||||
let debug = get().debug;
|
||||
return debug ? false: window.location.host.indexOf('localhost')!==-1;
|
||||
} ,
|
||||
host: () => {
|
||||
let debug = get().debug;
|
||||
return debug ? "wordpress.org" : window.location.host;
|
||||
},
|
||||
fetchSslData: async () => {
|
||||
rsssl_api.doAction('ssltest_get').then( ( response ) => {
|
||||
if (response.data.hasOwnProperty('host') ) {
|
||||
let data = get().processSslData(response.data);
|
||||
set({
|
||||
sslData: data,
|
||||
endpointData: data.endpointData,
|
||||
dataLoaded: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
getSslLabsData: (e) => {
|
||||
let clearCacheUrl = '';
|
||||
if (get().clearCache){
|
||||
set({clearCache:false,sslData:false });
|
||||
clearCacheUrl = '&startNew=on';
|
||||
}
|
||||
const url = "https://api.ssllabs.com/api/v3/analyze?host="+get().host()+clearCacheUrl;
|
||||
let data = {};
|
||||
data.url = url;
|
||||
return rsssl_api.doAction('ssltest_run', data).then( ( response ) => {
|
||||
if ( response && !response.errors) {
|
||||
return JSON.parse(response);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
},
|
||||
runSslTest: () => {
|
||||
get().getSslLabsData().then((sslData)=>{
|
||||
if ( sslData.status && sslData.status === 'ERROR' ){
|
||||
sslData = get().processSslData(sslData);
|
||||
set({
|
||||
sslData: sslData,
|
||||
sslScanStatus: 'completed',
|
||||
});
|
||||
clearInterval(get().intervalId);
|
||||
} else
|
||||
if ( sslData.endpoints && sslData.endpoints.filter((endpoint) => endpoint.statusMessage === 'Ready').length>0 ) {
|
||||
let completedEndpoints = sslData.endpoints.filter((endpoint) => endpoint.statusMessage === 'Ready');
|
||||
let lastCompletedEndpointIndex = completedEndpoints.length-1;
|
||||
let lastCompletedEndpoint = completedEndpoints[ lastCompletedEndpointIndex];
|
||||
let ipAddress = lastCompletedEndpoint.ipAddress;
|
||||
get().getEndpointData(ipAddress).then( (response ) => {
|
||||
let endpointData = get().endpointData;
|
||||
if (!Array.isArray(endpointData)) endpointData = [];
|
||||
if ( !response.errors ){
|
||||
//if the endpoint already is stored, replace it.
|
||||
let foundEndpoint = false;
|
||||
if (endpointData.length>0) {
|
||||
endpointData.forEach(function(endpoint, i) {
|
||||
if ( endpoint.ipAddress === response.ipAddress ) {
|
||||
endpointData[i] = response;
|
||||
foundEndpoint = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
if ( !foundEndpoint ) {
|
||||
endpointData[endpointData.length] = response;
|
||||
}
|
||||
set({endpointData: endpointData});
|
||||
sslData.endpointData = endpointData;
|
||||
}
|
||||
|
||||
if ( !sslData.errors ) {
|
||||
rsssl_api.doAction('store_ssl_labs', sslData );
|
||||
}
|
||||
sslData = get().processSslData(sslData);
|
||||
set({sslData: sslData, requestActive: false});
|
||||
});
|
||||
} else {
|
||||
//if there are no errors, this runs when the first endpoint is not completed yet
|
||||
sslData = get().processSslData(sslData);
|
||||
if ( !sslData.errors ) {
|
||||
rsssl_api.doAction('store_ssl_labs', sslData ).then( ( response ) => {});
|
||||
}
|
||||
set({sslData:sslData,requestActive: false});
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
processSslData: (sslData) => {
|
||||
if ( !sslData ) {
|
||||
sslData = {};
|
||||
}
|
||||
|
||||
let progress = sslData.progress ? sslData.progress : 0;
|
||||
let startTime = sslData.startTime ? sslData.startTime : '';
|
||||
let statusMessage = sslData.statusMessage ? sslData.statusMessage : '';
|
||||
let grade = sslData.grade ? sslData.grade : '?';
|
||||
let ipAddress='';
|
||||
if ( sslData.endpoints ) {
|
||||
let completedEndpoints = sslData.endpoints.filter((endpoint) => endpoint.statusMessage === 'Ready');
|
||||
let completedEndpointsLength = completedEndpoints.length;
|
||||
let lastCompletedEndpoint = completedEndpoints[ completedEndpointsLength-1];
|
||||
let activeEndpoint = sslData.endpoints.filter((endpoint) => endpoint.statusMessage === 'In progress')[0];
|
||||
let activeEndpointProgress = 0;
|
||||
if (activeEndpoint) {
|
||||
activeEndpointProgress = activeEndpoint.progress ? activeEndpoint.progress : 0;
|
||||
statusMessage = activeEndpoint.statusDetailsMessage;
|
||||
ipAddress = activeEndpoint.ipAddress;
|
||||
}
|
||||
if (lastCompletedEndpoint) grade = lastCompletedEndpoint.grade;
|
||||
|
||||
progress = ( completedEndpointsLength * 100 + activeEndpointProgress ) / sslData.endpoints.length;
|
||||
}
|
||||
if ( sslData.errors ) {
|
||||
grade = '?';
|
||||
statusMessage = sslData.errors[0].message;
|
||||
progress = 100;
|
||||
}
|
||||
let summary = {};
|
||||
if ( progress >= 100) {
|
||||
set({sslScanStatus: 'completed'});
|
||||
}
|
||||
summary.grade = grade;
|
||||
summary.startTime = startTime;
|
||||
summary.statusMessage = statusMessage;
|
||||
summary.ipAddress = ipAddress;
|
||||
summary.progress = progress;
|
||||
sslData.summary = summary;
|
||||
return sslData;
|
||||
},
|
||||
getEndpointData:(ipAddress) => {
|
||||
const url = 'https://api.ssllabs.com/api/v3/getEndpointData?host='+get().host()+'&s='+ipAddress;
|
||||
let data = {};
|
||||
data.url = url;
|
||||
return rsssl_api.doAction('ssltest_run', data).then( ( response ) => {
|
||||
if ( response && !response.errors) {
|
||||
return JSON.parse(response);
|
||||
}
|
||||
})
|
||||
}
|
||||
}));
|
||||
export default useSslLabs;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import useSslLabs from "./SslLabsData";
|
||||
const SslLabsFooter = () => {
|
||||
const {sslScanStatus, setSslScanStatus, isLocalHost} = useSslLabs();
|
||||
let disabled = sslScanStatus === 'active' || isLocalHost();
|
||||
return (
|
||||
<>
|
||||
<button disabled={disabled} onClick={ (e) => setSslScanStatus('active') } className="button button-default">
|
||||
{ sslScanStatus==='paused' && __("Continue SSL Health check", "really-simple-ssl")}
|
||||
{ sslScanStatus!=='paused' && __("Check SSL Health", "really-simple-ssl")}
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SslLabsFooter;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
const SslLabsHeader = () => {
|
||||
return (
|
||||
<>
|
||||
<h3 className="rsssl-grid-title rsssl-h4">{ __( "Status", 'really-simple-ssl' ) }</h3>
|
||||
<div className="rsssl-grid-item-controls">
|
||||
<span className="rsssl-header-html"> {__( "Powered by Qualys", 'really-simple-ssl' )}</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SslLabsHeader;
|
||||
@@ -0,0 +1,89 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import Icon from '../utils/Icon'
|
||||
import * as rsssl_api from "../utils/api";
|
||||
import useFields from "../Settings/FieldsData";
|
||||
import useProgress from "./Progress/ProgressData";
|
||||
import useMenu from "../Menu/MenuData";
|
||||
import DOMPurify from "dompurify";
|
||||
import {useState} from "@wordpress/element";
|
||||
|
||||
const TaskElement = (props) => {
|
||||
const {dismissNotice, getProgressData} = useProgress();
|
||||
const {getField, setHighLightField, showSavedSettingsNotice, updateFieldAttribute} = useFields();
|
||||
const {setSelectedSubMenuItem} = useMenu();
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const handleClick = async () => {
|
||||
setHighLightField(props.notice.output.highlight_field_id);
|
||||
let highlightField = getField(props.notice.output.highlight_field_id);
|
||||
await setSelectedSubMenuItem(highlightField.menu_id);
|
||||
}
|
||||
|
||||
const handleFix = (fix_id) => {
|
||||
let data = {};
|
||||
data.fix_id = fix_id;
|
||||
setProcessing(true);
|
||||
rsssl_api.doAction('fix', data).then( ( response ) => {
|
||||
setProcessing(false);
|
||||
showSavedSettingsNotice(response.msg);
|
||||
getProgressData();
|
||||
});
|
||||
}
|
||||
|
||||
const handleClearCache = async (cache_id) => {
|
||||
setProcessing(true)
|
||||
let data = {};
|
||||
data.cache_id = cache_id;
|
||||
try {
|
||||
// First clear the cache on the server
|
||||
await rsssl_api.doAction('clear_cache', data);
|
||||
|
||||
// If this is the 404 resources cache clear, update our field states
|
||||
if (cache_id === 'rsssl_homepage_contains_404_resources') {
|
||||
// Update the disabled state for both 404-related fields
|
||||
updateFieldAttribute('404_blocking_threshold', 'disabled', false);
|
||||
updateFieldAttribute('404_blocking_lockout_duration', 'disabled', false);
|
||||
}
|
||||
|
||||
// Show notice and refresh progress data
|
||||
showSavedSettingsNotice(__('Re-started test', 'really-simple-ssl'));
|
||||
await getProgressData();
|
||||
} catch (error) {
|
||||
console.error('Error clearing cache:', error);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
}
|
||||
|
||||
let notice = props.notice;
|
||||
let premium = notice.output.icon==='premium';
|
||||
//treat links to rsssl.com and internal links different.
|
||||
let urlIsExternal = notice.output.url && notice.output.url.indexOf('really-simple-ssl.com') !== -1;
|
||||
return(
|
||||
<div className="rsssl-task-element">
|
||||
<span className={'rsssl-task-status rsssl-' + notice.output.icon}>{ notice.output.label }</span>
|
||||
<p className="rsssl-task-message" dangerouslySetInnerHTML={{__html: DOMPurify.sanitize( notice.output.msg )}}></p> {/* nosemgrep: react-dangerouslysetinnerhtml */}
|
||||
{urlIsExternal && notice.output.url && <a target="_blank" rel="noopener noreferrer" href={notice.output.url}>{__("More info", "really-simple-ssl")}</a> }
|
||||
{notice.output.clear_cache_id && <span className="rsssl-task-enable button button-secondary" onClick={ () => handleClearCache(notice.output.clear_cache_id ) }>{__("Re-check", "really-simple-ssl")}</span> }
|
||||
{notice.output.fix_id && <span className="rsssl-task-enable button button-secondary" onClick={ () => handleFix(notice.output.fix_id ) }>
|
||||
{!processing && __("Fix", "really-simple-ssl")}
|
||||
{processing && <Icon name = "loading" color = 'black' size={14} />}
|
||||
</span> }
|
||||
{!premium && !urlIsExternal && notice.output.url && <a className="rsssl-task-enable button button-secondary" href={notice.output.url}>
|
||||
{!processing && __("View", "really-simple-ssl")}
|
||||
{processing && <Icon name = "loading" color = 'black' size={14} />}
|
||||
</a> }
|
||||
|
||||
{!premium && notice.output.highlight_field_id && <span className="rsssl-task-enable button button-secondary" onClick={() => handleClick()}>{__("View", "really-simple-ssl")}</span> }
|
||||
{notice.output.plusone && <span className='rsssl-plusone'>1</span>}
|
||||
{notice.output.dismissible && notice.output.status!=='completed' &&
|
||||
<div className="rsssl-task-dismiss">
|
||||
<button type='button' onClick={(e) => dismissNotice(notice.id) }>
|
||||
<Icon name='times' />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TaskElement;
|
||||
@@ -0,0 +1,47 @@
|
||||
import {addUrlRef} from "../../utils/AddUrlRef";
|
||||
|
||||
const Tip = ({link, content}) => {
|
||||
return (
|
||||
<div className="rsssl-tips-tricks-element">
|
||||
<a href={link} target="_blank" rel="noopener noreferrer" title={content}>
|
||||
<div className="rsssl-icon">
|
||||
<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="15">
|
||||
<path fill="var(--rsp-grey-300)" d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32s32 14.3 32 32s-14.3 32-32 32z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="rsssl-tips-tricks-content">{content}</div>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const TipsTricks = () => {
|
||||
const items = [
|
||||
{
|
||||
content: "Why WordPress is (in)secure",
|
||||
link: 'https://really-simple-ssl.com/why-wordpress-is-insecure/',
|
||||
}, {
|
||||
content: "Always be ahead of vulnerabilities",
|
||||
link: 'https://really-simple-ssl.com/staying-ahead-of-vulnerabilities/',
|
||||
}, {
|
||||
content: "Harden your website's security",
|
||||
link: 'https://really-simple-ssl.com/hardening-your-websites-security/',
|
||||
}, {
|
||||
content: "Login protection as essential security",
|
||||
link: 'https://really-simple-ssl.com/login-protection-as-essential-security/',
|
||||
}, {
|
||||
content: "Protect site visitors with Security Headers",
|
||||
link: 'https://really-simple-ssl.com/protecting-site-visitors-with-security-headers',
|
||||
}, {
|
||||
content: "Enable an efficient and performant firewall",
|
||||
link: 'https://really-simple-ssl.com/enable-an-efficient-and-performant-firewall/',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="rsssl-tips-tricks-container">
|
||||
{items.map((item, i) => <Tip key={"trick-"+i} link={addUrlRef(item.link)} content={item.content} /> ) }
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
export default TipsTricks
|
||||
@@ -0,0 +1,15 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {addUrlRef} from "../../utils/AddUrlRef";
|
||||
|
||||
const TipsTricksFooter = () => {
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<a href={addUrlRef("https://really-simple-ssl.com/knowledge-base-overview/")} target="_blank"rel="noopener noreferrer" className="button button-secondary">{ __("Documentation", "really-simple-ssl")}</a>
|
||||
</>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
export default TipsTricksFooter
|
||||
@@ -0,0 +1,309 @@
|
||||
import Icon from "../../utils/Icon";
|
||||
import {__, _n} from "@wordpress/i18n";
|
||||
import {useEffect, useState} from "@wordpress/element";
|
||||
import useFields from "../../Settings/FieldsData";
|
||||
import useRiskData from "../../Settings/RiskConfiguration/RiskData";
|
||||
|
||||
const Vulnerabilities = () => {
|
||||
const {
|
||||
vulnerabilities,
|
||||
vulnerabilityScore,
|
||||
updates,
|
||||
dataLoaded,
|
||||
fetchVulnerabilities
|
||||
} = useRiskData();
|
||||
const {fields, getFieldValue} = useFields();
|
||||
const [vulnerabilityWord, setVulnerabilityWord] = useState('');
|
||||
const [updateWord, setUpdateWord] = useState('');
|
||||
const [updateWordCapitalized, setUpdateWordCapitalized] = useState('');
|
||||
const [vulnerabilityWordCapitalized, setVulnerabilityWordCapitalized] = useState('');
|
||||
const [updateString, setUpdateString] = useState('');
|
||||
const [hardeningWord, setHardeningWord] = useState('');
|
||||
const [notEnabledHardeningFields, setNotEnabledHardeningFields] = useState(0);
|
||||
const [vulEnabled, setVulEnabled] = useState(false);
|
||||
useEffect(() => {
|
||||
if (getFieldValue('enable_vulnerability_scanner')==1) {
|
||||
setVulEnabled(true);
|
||||
}
|
||||
}, [fields]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dataLoaded) {
|
||||
fetchVulnerabilities();
|
||||
}
|
||||
}, [vulEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
//singular or plural of the word vulnerability
|
||||
const v = (vulnerabilities === 1) ? __("vulnerability", "really-simple-ssl") : __("vulnerabilities", "really-simple-ssl");
|
||||
setVulnerabilityWordCapitalized(v.charAt(0).toUpperCase() + v.slice(1));
|
||||
setVulnerabilityWord(v);
|
||||
const u = (updates === 1) ? __("update", "really-simple-ssl") : __("updates", "really-simple-ssl");
|
||||
const s = _n('You have %s update pending', 'You have %s updates pending', updates, 'really-simple-ssl').replace('%s', updates);
|
||||
setUpdateWord(u);
|
||||
setUpdateWordCapitalized(u.charAt(0).toUpperCase() + u.slice(1));
|
||||
setUpdateString(s);
|
||||
const h = (notEnabledHardeningFields === 1) ? __("hardening feature", "really-simple-ssl") : __("hardening features", "really-simple-ssl");
|
||||
setHardeningWord(h);
|
||||
},[vulnerabilities, updates, notEnabledHardeningFields])
|
||||
|
||||
useEffect(() => {
|
||||
if (fields.length>0) {
|
||||
let notEnabledFields = fields.filter(field => field.recommended);
|
||||
//we need to filter enabled fields, but also disabled fields, because these are not enabled, but set by another method, so disabled
|
||||
notEnabledFields = notEnabledFields.filter(field => field.value !== 1 && field.disabled !== true);
|
||||
setNotEnabledHardeningFields(notEnabledFields.length);
|
||||
}
|
||||
},[fields])
|
||||
|
||||
let vulClass = 'rsssl-inactive';
|
||||
let badgeVulStyle = vulEnabled?'rsp-success':'rsp-default';
|
||||
let badgeUpdateStyle = 'rsp-success';
|
||||
let iconVulColor = 'green';
|
||||
let iconVulEnabledColor = 'red';
|
||||
let iconUpdateColor = 'black';
|
||||
if (vulEnabled || notEnabledHardeningFields > 0 || updates > 0) {
|
||||
//now we calculate the score
|
||||
let score = vulnerabilityScore();
|
||||
//we create correct badge style
|
||||
if (score >= 5) {
|
||||
badgeVulStyle = 'rsp-critical';
|
||||
iconVulColor = 'red';
|
||||
} else if (score < 4 && score > 0) {
|
||||
badgeVulStyle = 'rsp-medium';
|
||||
iconVulColor = 'yellow';
|
||||
}
|
||||
|
||||
if (updates >= 5) {
|
||||
badgeUpdateStyle = 'rsp-critical';
|
||||
iconUpdateColor = 'red';
|
||||
} else if (score < 5 && score > 0) {
|
||||
badgeUpdateStyle = 'rsp-medium';
|
||||
iconUpdateColor = 'yellow';
|
||||
}
|
||||
|
||||
if ( score < notEnabledHardeningFields ) {
|
||||
score = notEnabledHardeningFields;
|
||||
}
|
||||
|
||||
if (score < updates) {
|
||||
score = updates;
|
||||
}
|
||||
|
||||
if (score === 0) {
|
||||
vulClass = 'rsssl-success';
|
||||
} else if (score < 5) {
|
||||
vulClass = 'rsssl-warning';
|
||||
} else {
|
||||
vulClass = 'rsssl-error';
|
||||
}
|
||||
|
||||
// if (!vulEnabled) vulClass = "rsssl-inactive";
|
||||
|
||||
}
|
||||
|
||||
const checkVulActive = () => {
|
||||
if (vulEnabled) {
|
||||
// iconVulEnabledColor = 'green';
|
||||
return (<></>)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-hardening-list-item">
|
||||
<Icon name="info" color='yellow'/>
|
||||
<p className={'rsssl-hardening-list-item-text'}> {__("Enable vulnerability detection", "really-simple-ssl")}</p>
|
||||
<a href="#settings/vulnerabilities">{__("Enable", "really-simple-ssl")}</a>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const checkUpdates = () => {
|
||||
let icon = 'circle-check';
|
||||
let iconColor = 'green';
|
||||
if (updates > 0) {
|
||||
icon = 'info';
|
||||
iconColor = 'yellow';
|
||||
}
|
||||
if (updates >= 5) {
|
||||
icon = 'circle-times';
|
||||
iconColor = 'red';
|
||||
}
|
||||
|
||||
if (updates) {
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-hardening-list-item">
|
||||
<Icon name={icon} color={iconColor}/>
|
||||
<p className="rsssl-hardening-list-item-text">
|
||||
{updateString}
|
||||
</p>
|
||||
<a href={rsssl_settings.plugins_url + "?plugin_status=upgrade"}
|
||||
style={linkStyle}>{updateWordCapitalized}</a>
|
||||
</div>
|
||||
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-hardening-list-item">
|
||||
<Icon name={icon} color={iconColor}/>
|
||||
<p className="rsssl-hardening-list-item-text">
|
||||
{updateString}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const checkVul = () => {
|
||||
let icon = 'circle-check';
|
||||
let iconColor = 'green';
|
||||
if (vulnerabilityScore() > 0) {
|
||||
icon = 'info';
|
||||
iconColor = 'yellow';
|
||||
}
|
||||
if (vulnerabilityScore() >= 5) {
|
||||
icon = 'circle-times';
|
||||
iconColor = 'red';
|
||||
}
|
||||
if (!vulEnabled) {
|
||||
return (
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (vulnerabilities) {
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-hardening-list-item">
|
||||
<Icon name={icon} color={iconColor}/>
|
||||
<p className="rsssl-hardening-list-item-text">
|
||||
{__("You have %s %d", "really-simple-ssl")
|
||||
.replace("%s", vulnerabilities)
|
||||
.replace("%d", vulnerabilityWord)
|
||||
}
|
||||
</p>
|
||||
<a style={linkStyle} href={'#settings/vulnerabilities'}>{__('Learn more', 'really-simple-ssl')}</a>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-hardening-list-item">
|
||||
<Icon name="circle-check" color='green'/>
|
||||
<p className="rsssl-hardening-list-item-text">
|
||||
{__("You have %s %d", "really-simple-ssl")
|
||||
.replace("%d", vulnerabilityWord)
|
||||
.replace("%s", vulnerabilities)
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const linkStyle = {
|
||||
marginLeft: '0.3em'
|
||||
}
|
||||
const checknotEnabledHardeningFields = () => {
|
||||
if (notEnabledHardeningFields) {
|
||||
let icon = 'circle-check';
|
||||
let iconColor = 'green';
|
||||
if (notEnabledHardeningFields > 0) {
|
||||
icon = 'info';
|
||||
iconColor = 'yellow';
|
||||
}
|
||||
if (notEnabledHardeningFields >= 5) {
|
||||
icon = 'circle-times';
|
||||
iconColor = 'red';
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-hardening-list-item">
|
||||
<Icon name={icon} color={iconColor}/>
|
||||
|
||||
<p className={"rsssl-hardening-list-item-text"}>
|
||||
{__("You have %s open %d", "really-simple-ssl").replace("%s", notEnabledHardeningFields).replace('%d',hardeningWord)}
|
||||
</p>
|
||||
<a href="#settings/hardening">{__("Settings", "really-simple-ssl")}</a>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (<>
|
||||
<div className="rsssl-hardening-list-item">
|
||||
<Icon name="circle-check" color='green'/>
|
||||
<p className={"rsssl-hardening-list-item-text"}>{__("Hardening features are configured", "really-simple-ssl")}</p>
|
||||
</div>
|
||||
</>)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{dataLoaded ?
|
||||
<div className={'rsssl-hardening'}>
|
||||
<div className="rsssl-gridblock-progress" ></div>
|
||||
<div className={"rsssl-hardening-select " + vulClass}>
|
||||
<div className="rsssl-hardening-select-item">
|
||||
{vulEnabled ? <Icon color={iconVulColor} size={23} name="radar-duotone"></Icon> : <Icon size={23} color={iconVulEnabledColor} name="satellite-dish-duotone"></Icon>}
|
||||
<h2>{vulEnabled ? vulnerabilities : '?'}</h2>
|
||||
<span className={"rsssl-badge " + badgeVulStyle}>{vulnerabilityWordCapitalized}</span>
|
||||
</div>
|
||||
<div className="rsssl-hardening-select-item">
|
||||
{ updates ? <Icon size={23} color={iconUpdateColor} name="rotate-exclamation-light"></Icon> : <Icon size={23} color={'black'} name="rotate-light"></Icon>}
|
||||
<h2>{updates}</h2>
|
||||
<span className={"rsssl-badge " + badgeUpdateStyle}>{updateWordCapitalized}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rsssl-hardening-list">
|
||||
{checknotEnabledHardeningFields()}
|
||||
{checkVulActive()}
|
||||
{checkVul()}
|
||||
{checkUpdates()}
|
||||
</div>
|
||||
</div>
|
||||
: <div className="rsssl-hardening">
|
||||
<div className="rsssl-gridblock-progress" ></div>
|
||||
<div className="rsssl-hardening-select">
|
||||
<div className="rsssl-hardening-select-item">
|
||||
<Icon size={23} color={'grey'} name="radar-duotone"></Icon>
|
||||
<h2>0</h2>
|
||||
<span className={"rsssl-badge rsp-default"}>{vulnerabilityWordCapitalized}</span>
|
||||
</div>
|
||||
<div className="rsssl-hardening-select-item">
|
||||
<Icon size={23} color={'grey'} name="rotate-exclamation-light"></Icon>
|
||||
<h2>0</h2>
|
||||
<span className={"rsssl-badge rsp-default"}>{updateWordCapitalized}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rsssl-hardening-list">
|
||||
<div className="rsssl-hardening-list-item">
|
||||
<Icon color={'grey'} name="circle-check"></Icon>
|
||||
<p className={"rsssl-hardening-list-item-text"}>{__("Loading...", "really-simple-ssl")}</p>
|
||||
</div>
|
||||
<div className="rsssl-hardening-list-item">
|
||||
<Icon color={'grey'} name="circle-check"></Icon>
|
||||
<p className={"rsssl-hardening-list-item-text"}>{__("Loading...", "really-simple-ssl")}</p>
|
||||
</div>
|
||||
<div className="rsssl-hardening-list-item">
|
||||
<Icon color={'grey'} name="circle-check"></Icon>
|
||||
<p className={"rsssl-hardening-list-item-text"}>{__("Loading...", "really-simple-ssl")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Vulnerabilities;
|
||||
@@ -0,0 +1,30 @@
|
||||
import {useEffect, useState} from '@wordpress/element';
|
||||
import {__} from '@wordpress/i18n';
|
||||
import useRiskData from "../../Settings/RiskConfiguration/RiskData";
|
||||
import useFields from "../../Settings/FieldsData";
|
||||
import {getRelativeTime} from '../../utils/formatting';
|
||||
|
||||
const VulnerabilitiesFooter = (props) => {
|
||||
const {lastChecked} = useRiskData();
|
||||
const {fields, getFieldValue} = useFields();
|
||||
const [vulEnabled, setVulEnabled] = useState(false);
|
||||
useEffect(() => {
|
||||
if (getFieldValue('enable_vulnerability_scanner')==1) {
|
||||
setVulEnabled(true);
|
||||
}
|
||||
}, [fields]);
|
||||
|
||||
const styleFooter = {
|
||||
textAlign: 'right',
|
||||
position: 'relative',
|
||||
right: '0',
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<a href="#settings/vulnerabilities" className={'button button-default'}>{__('Settings', 'really-simple-ssl')}</a>
|
||||
{vulEnabled? <p className={'rsssl-small-text'}>{getRelativeTime(lastChecked)}</p>: null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default VulnerabilitiesFooter;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {useEffect, useState} from "@wordpress/element";
|
||||
import useFields from "../../Settings/FieldsData";
|
||||
const VulnerabilitiesHeader = () => {
|
||||
const {fields, getFieldValue} = useFields();
|
||||
const [vulEnabled, setVulEnabled] = useState(false);
|
||||
useEffect(() => {
|
||||
if (getFieldValue('enable_vulnerability_scanner')==1) {
|
||||
setVulEnabled(true);
|
||||
}
|
||||
}, [fields]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="rsssl-grid-title rsssl-h4">{ vulEnabled ? __( "Vulnerabilities", 'really-simple-ssl' ) : __( "Hardening", 'really-simple-ssl' ) }</h3>
|
||||
<div className="rsssl-grid-item-controls">
|
||||
<span className="rsssl-header-html"></span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default VulnerabilitiesHeader;
|
||||
@@ -0,0 +1,63 @@
|
||||
import {useEffect} from "@wordpress/element";
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import Notices from "./Settings/Notices";
|
||||
import useMenu from "./Menu/MenuData";
|
||||
import {addUrlRef} from "./utils/AddUrlRef";
|
||||
|
||||
const Header = () => {
|
||||
const {menu, selectedMainMenuItem, fetchMenuData} = useMenu();
|
||||
let plugin_url = rsssl_settings.plugin_url;
|
||||
useEffect( () => {
|
||||
fetchMenuData();
|
||||
}, [] );
|
||||
|
||||
let menuItems = menu.filter( item => item!==null );
|
||||
return (
|
||||
<div className="rsssl-header-container">
|
||||
<div className="rsssl-header">
|
||||
<img className="rsssl-logo" src={plugin_url+"assets/img/really-simple-security-logo.svg"} alt="Really Simple Security logo" />
|
||||
<div className="rsssl-header-left">
|
||||
<nav className="rsssl-header-menu">
|
||||
<ul>
|
||||
{menuItems.map((menu_item, i) =>
|
||||
<li key={"menu-"+i}><a className={ selectedMainMenuItem === menu_item.id ? 'active' : '' } href={"#" + menu_item.id.toString()} >{menu_item.title}</a></li>)}
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="rsssl-header-right">
|
||||
{ !rsssl_settings.le_generated_by_rsssl &&
|
||||
<a className="rsssl-knowledge-base-link" href={addUrlRef("https://really-simple-ssl.com/knowledge-base")} target="_blank" rel="noopener noreferrer">{__("Documentation", "really-simple-ssl")}</a>}
|
||||
{ rsssl_settings.le_generated_by_rsssl &&
|
||||
<a href={rsssl_settings.letsencrypt_url}>{__("Let's Encrypt","really-simple-ssl")}</a>
|
||||
}
|
||||
{rsssl_settings.pro_plugin_active && (
|
||||
<>
|
||||
{(() => {
|
||||
const supportUrl = rsssl_settings.dashboard_url + '#settings&highlightfield=premium_support';
|
||||
return (
|
||||
<a
|
||||
href={supportUrl}
|
||||
className="button button-black"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{__("Support", "really-simple-ssl")}
|
||||
</a>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
)}
|
||||
{ !rsssl_settings.pro_plugin_active &&
|
||||
<a href={rsssl_settings.upgrade_link}
|
||||
className="button button-black"
|
||||
target="_blank" rel="noopener noreferrer">{__("Go Pro", "really-simple-ssl")}</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<Notices className="rsssl-wizard-notices"/>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
export default Header
|
||||
@@ -0,0 +1,11 @@
|
||||
import Onboarding from "../Onboarding/Onboarding";
|
||||
|
||||
const Activate = () => {
|
||||
return (
|
||||
<div className="rsssl-lets-encrypt-tests">
|
||||
<Onboarding/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Activate;
|
||||
@@ -0,0 +1,167 @@
|
||||
import {__} from '@wordpress/i18n';
|
||||
import Hyperlink from "../utils/Hyperlink";
|
||||
import {
|
||||
Button,
|
||||
} from '@wordpress/components';
|
||||
import useFields from "../Settings/FieldsData";
|
||||
import useMenu from "../Menu/MenuData";
|
||||
import {useEffect} from '@wordpress/element';
|
||||
import useLetsEncryptData from "./letsEncryptData";
|
||||
import {addUrlRef} from "../utils/AddUrlRef";
|
||||
|
||||
const Directories = ({action, field}) => {
|
||||
const {switchButtonDisabled, updateVerificationType, setRefreshTests} = useLetsEncryptData();
|
||||
|
||||
const {addHelpNotice, updateField, setChangedField, saveFields, fetchFieldsData} = useFields();
|
||||
const { setSelectedSubMenuItem} = useMenu();
|
||||
|
||||
useEffect(() => {
|
||||
if ((action && action.action === 'challenge_directory_reachable' && action.status === 'error')) {
|
||||
addHelpNotice(
|
||||
field.id,
|
||||
'default',
|
||||
__("The challenge directory is used to verify the domain ownership.", "really-simple-ssl"),
|
||||
);
|
||||
}
|
||||
|
||||
if ((action && action.action === 'check_key_directory' && action.status === 'error')) {
|
||||
addHelpNotice(
|
||||
field.id,
|
||||
'default',
|
||||
__("The key directory is needed to store the generated keys.", "really-simple-ssl") + ' ' + __("By placing it outside the root folder, it is not publicly accessible.", "really-simple-ssl"),
|
||||
);
|
||||
}
|
||||
|
||||
if ((action && action.action === 'check_certs_directory' && action.status === 'error')) {
|
||||
addHelpNotice(
|
||||
field.id,
|
||||
'default',
|
||||
__("The certificate will get stored in this directory.", "really-simple-ssl") + ' ' + __("By placing it outside the root folder, it is not publicly accessible.", "really-simple-ssl"),
|
||||
);
|
||||
}
|
||||
}, [action]);
|
||||
|
||||
|
||||
if ( !action ) {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
const handleSwitchToDNS = async () => {
|
||||
|
||||
updateField('verification_type', 'dns');
|
||||
setChangedField('verification_type', 'dns');
|
||||
await saveFields(true, true);
|
||||
await updateVerificationType('dns');
|
||||
await setSelectedSubMenuItem('le-dns-verification');
|
||||
await fetchFieldsData('le-dns-verification');
|
||||
setRefreshTests(true);
|
||||
}
|
||||
|
||||
let dirError = action.status === 'error' && action.action === 'challenge_directory_reachable';
|
||||
|
||||
return (
|
||||
<div className="rsssl-test-results">
|
||||
{action.status === 'error' && <h4>{__("Next step", "really-simple-ssl")}</h4>}
|
||||
{!dirError && rsssl_settings.hosting_dashboard === 'cpanel' &&
|
||||
<><p>
|
||||
<Hyperlink target="_blank" rel="noopener noreferrer"
|
||||
text={__("If you also want to secure subdomains like mail.domain.com, cpanel.domain.com, you have to use the %sDNS%s challenge.", "really-simple-ssl")}
|
||||
url={addUrlRef("https://really-simple-ssl.com/lets-encrypt-authorization-with-dns")}/>
|
||||
{__("Please note that auto-renewal with a DNS challenge might not be possible.", "really-simple-ssl")}
|
||||
</p></>
|
||||
}
|
||||
<div>
|
||||
<p>
|
||||
{__("If the challenge directory cannot be created, or is not reachable, you can either remove the server limitation, or change to DNS verification.", "really-simple-ssl")}
|
||||
</p>
|
||||
<Button
|
||||
disabled={switchButtonDisabled}
|
||||
variant="secondary"
|
||||
onClick={() => handleSwitchToDNS()}
|
||||
>
|
||||
{__('Switch to DNS verification', 'really-simple-ssl')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{(action.status === 'error' && action.action === 'check_challenge_directory') &&
|
||||
<div>
|
||||
<h4>
|
||||
{__("Create a challenge directory", "really-simple-ssl")}
|
||||
</h4>
|
||||
<p>
|
||||
{__("Navigate in FTP or File Manager to the root of your WordPress installation:", "really-simple-ssl")}
|
||||
</p>
|
||||
<ul>
|
||||
<li className="rsssl-tooltip-icon dashicons-before rsssl-icon arrow-right-alt2 dashicons-arrow-right-alt2">
|
||||
{__('Create a folder called “.well-known”', 'really-simple-ssl')}
|
||||
</li>
|
||||
<li className="rsssl-tooltip-icon dashicons-before rsssl-icon arrow-right-alt2 dashicons-arrow-right-alt2">
|
||||
{__('Inside the folder called “.well-known” create a new folder called “acme-challenge”, with 644 writing permissions.', 'really-simple-ssl')}
|
||||
</li>
|
||||
<li className="rsssl-tooltip-icon dashicons-before rsssl-icon arrow-right-alt2 dashicons-arrow-right-alt2">
|
||||
{__('Click the refresh button.', 'really-simple-ssl')}
|
||||
</li>
|
||||
</ul>
|
||||
<h4>
|
||||
{__("Or you can switch to DNS verification", "really-simple-ssl")}
|
||||
</h4>
|
||||
<p>{__("If the challenge directory cannot be created, you can either remove the server limitation, or change to DNS verification.", "really-simple-ssl")}</p>
|
||||
<Button
|
||||
disabled={switchButtonDisabled}
|
||||
variant="secondary"
|
||||
onClick={() => handleSwitchToDNS()}
|
||||
>
|
||||
{__('Switch to DNS verification', 'really-simple-ssl')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
{(action.status === 'error' && action.action === 'check_key_directory') &&
|
||||
<div>
|
||||
<h4>
|
||||
{__("Create a key directory", "really-simple-ssl")}
|
||||
</h4>
|
||||
<p>
|
||||
{__("Navigate in FTP or File Manager to one level above the root of your WordPress installation:", "really-simple-ssl")}
|
||||
</p>
|
||||
<ul>
|
||||
<li className="rsssl-tooltip-icon dashicons-before rsssl-icon arrow-right-alt2 dashicons-arrow-right-alt2">
|
||||
{__('Create a folder called “ssl”', 'really-simple-ssl')}
|
||||
</li>
|
||||
<li className="rsssl-tooltip-icon dashicons-before rsssl-icon arrow-right-alt2 dashicons-arrow-right-alt2">
|
||||
{__('Inside the folder called “ssl” create a new folder called “keys”, with 644 writing permissions.', 'really-simple-ssl')}
|
||||
</li>
|
||||
<li className="rsssl-tooltip-icon dashicons-before rsssl-icon arrow-right-alt2 dashicons-arrow-right-alt2">
|
||||
{__('Click the refresh button.', 'really-simple-ssl')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
{(action.status === 'error' && action.action === 'check_certs_directory') &&
|
||||
<div>
|
||||
<h4>
|
||||
{__("Create a certs directory", "really-simple-ssl")}
|
||||
</h4>
|
||||
<p>
|
||||
{__("Navigate in FTP or File Manager to one level above the root of your WordPress installation:", "really-simple-ssl")}
|
||||
</p>
|
||||
<ul>
|
||||
<li className="rsssl-tooltip-icon dashicons-before rsssl-icon arrow-right-alt2 dashicons-arrow-right-alt2">
|
||||
{__('Create a folder called “ssl”', 'really-simple-ssl')}
|
||||
</li>
|
||||
<li className="rsssl-tooltip-icon dashicons-before rsssl-icon arrow-right-alt2 dashicons-arrow-right-alt2">
|
||||
{__('Inside the folder called “ssl” create a new folder called “certs”, with 644 writing permissions.', 'really-simple-ssl')}
|
||||
</li>
|
||||
<li className="rsssl-tooltip-icon dashicons-before rsssl-icon arrow-right-alt2 dashicons-arrow-right-alt2">
|
||||
{__('Click the refresh button.', 'really-simple-ssl')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Directories;
|
||||
@@ -0,0 +1,88 @@
|
||||
import {useState, useEffect} from "@wordpress/element";
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import Hyperlink from "../utils/Hyperlink";
|
||||
import {
|
||||
Button,
|
||||
} from '@wordpress/components';
|
||||
import useFields from "../Settings/FieldsData";
|
||||
import useMenu from "../Menu/MenuData";
|
||||
import useLetsEncryptData from "./letsEncryptData";
|
||||
import {addUrlRef} from "../utils/AddUrlRef";
|
||||
|
||||
const DnsVerification = (props) => {
|
||||
const {switchButtonDisabled, updateVerificationType, setRefreshTests} = useLetsEncryptData();
|
||||
const {fields, addHelpNotice, updateField, setChangedField, saveFields, fetchFieldsData, getFieldValue} = useFields();
|
||||
const {selectedSubMenuItem, setSelectedSubMenuItem} = useMenu();
|
||||
const [tokens, setTokens] = useState(false);
|
||||
let action = props.action;
|
||||
|
||||
useEffect(()=> {
|
||||
if (action && action.action==='challenge_directory_reachable' && action.status==='error') {
|
||||
addHelpNotice(
|
||||
props.field.id,
|
||||
'default',
|
||||
__("The challenge directory is used to verify the domain ownership.", "really-simple-ssl"),
|
||||
);
|
||||
}
|
||||
let newTokens = action ? action.output : false;
|
||||
if ( typeof (newTokens) === "undefined" || newTokens.length === 0 ) {
|
||||
newTokens = false;
|
||||
}
|
||||
if ( newTokens ) {
|
||||
setTokens(newTokens);
|
||||
}
|
||||
}, [action]);
|
||||
|
||||
const handleSwitchToDir = async () => {
|
||||
await setSelectedSubMenuItem('le-directories');
|
||||
await updateField('verification_type', 'dir');
|
||||
await setChangedField('verification_type', 'dir');
|
||||
await saveFields(true, true);
|
||||
await updateVerificationType('dir');
|
||||
await fetchFieldsData('le-directories');
|
||||
setRefreshTests(true);
|
||||
}
|
||||
|
||||
let verificationType = getFieldValue('verification_type');
|
||||
if (verificationType==='dir') {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ tokens && tokens.length>0 &&
|
||||
<div className="rsssl-test-results">
|
||||
<h4>{__("Next step", "really-simple-ssl")}</h4>
|
||||
<p>{__("Add the following token as text record to your DNS records. We recommend to use a short TTL during installation, in case you need to change it.", "really-simple-ssl")}
|
||||
<Hyperlink target="_blank" rel="noopener noreferrer" text={__("Read more", "really-simple-ssl")}
|
||||
url={addUrlRef("https://really-simple-ssl.com/how-to-add-a-txt-record-to-dns")}/>
|
||||
</p>
|
||||
<div className="rsssl-dns-text-records">
|
||||
<div>
|
||||
<div className="rsssl-dns-domain">@/{__("Domain", "really-simple-ssl")}</div>
|
||||
<div className="rsssl-dns-field">{__("Value", "really-simple-ssl")}</div>
|
||||
</div>
|
||||
{ tokens.map((tokenData, i) =>
|
||||
<div>
|
||||
<div className="rsssl-dns-">_acme-challenge.{tokenData.domain}</div>
|
||||
<div className="rsssl-dns-field rsssl-selectable">{tokenData.token}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="rsssl-test-results">
|
||||
<p>{__("DNS verification active. You can switch back to directory verification here.","really-simple-ssl")}</p>
|
||||
<Button
|
||||
disabled={switchButtonDisabled}
|
||||
variant="secondary"
|
||||
onClick={() => handleSwitchToDir()}
|
||||
>{ __( 'Switch to directory verification', 'really-simple-ssl' ) }</Button>
|
||||
</div>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DnsVerification;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import * as rsssl_api from "../utils/api";
|
||||
import {dispatch,} from '@wordpress/data';
|
||||
import sleeper from "../utils/sleeper";
|
||||
import Hyperlink from "../utils/Hyperlink";
|
||||
|
||||
import {
|
||||
Button,
|
||||
} from '@wordpress/components';
|
||||
import useFields from "../Settings/FieldsData";
|
||||
|
||||
const Generation = (props) => {
|
||||
let action = props.action;
|
||||
|
||||
if (!action) {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
const handleSkipDNS = () => {
|
||||
return rsssl_api.runLetsEncryptTest('skip_dns_check').then( ( response ) => {
|
||||
props.restartTests();
|
||||
const notice = dispatch('core/notices').createNotice(
|
||||
'success',
|
||||
__( 'Skip DNS verification', 'really-simple-ssl' ),
|
||||
{
|
||||
__unstableHTML: true,
|
||||
id: 'rsssl_skip_dns',
|
||||
type: 'snackbar',
|
||||
isDismissible: true,
|
||||
}
|
||||
).then(sleeper(3000)).then(( response ) => {
|
||||
dispatch('core/notices').removeNotice('rsssl_skip_dns');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rsssl-test-results">
|
||||
{ (action.status === 'error' && action.action==='verify_dns' ) &&
|
||||
<>
|
||||
<p>{ __("We could not check the DNS records. If you just added the record, please check in a few minutes.","really-simple-ssl")}
|
||||
<Hyperlink target="_blank" rel="noopener noreferrer" text={__("You can manually check the DNS records in an %sonline tool%s.","really-simple-ssl")}
|
||||
url="https://mxtoolbox.com/SuperTool.aspx"/>
|
||||
{ ' '+__("If you're sure it's set correctly, you can click the button to skip the DNS check.","really-simple-ssl")}
|
||||
</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleSkipDNS()}
|
||||
>
|
||||
{ __( 'Skip DNS check', 'really-simple-ssl' ) }
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Generation;
|
||||
@@ -0,0 +1,112 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import * as rsssl_api from "../utils/api";
|
||||
import {dispatch,} from '@wordpress/data';
|
||||
import {useEffect, useState} from '@wordpress/element';
|
||||
import sleeper from "../utils/sleeper";
|
||||
import useFields from "../Settings/FieldsData";
|
||||
|
||||
const Installation = (props) => {
|
||||
const {addHelpNotice} = useFields();
|
||||
|
||||
const [installationData, setInstallationData] = useState(false);
|
||||
let action = props.action;
|
||||
|
||||
useEffect(()=> {
|
||||
if ((action && action.status==='warning' && installationData && installationData.generated_by_rsssl )) {
|
||||
addHelpNotice(
|
||||
props.field.id,
|
||||
'default',
|
||||
__("This is the certificate, which you need to install in your hosting dashboard.", "really-simple-ssl"),
|
||||
__("Certificate (CRT)", "really-simple-ssl")
|
||||
);
|
||||
|
||||
addHelpNotice(
|
||||
props.field.id,
|
||||
'default',
|
||||
__("The private key can be uploaded or pasted in the appropriate field on your hosting dashboard.", "really-simple-ssl"),
|
||||
__("Private Key (KEY)", "really-simple-ssl")
|
||||
);
|
||||
|
||||
addHelpNotice(
|
||||
props.field.id,
|
||||
'default',
|
||||
__("The CA Bundle will sometimes be automatically detected. If not, you can use this file.", "really-simple-ssl"),
|
||||
__("Certificate Authority Bundle (CABUNDLE)", "really-simple-ssl")
|
||||
);
|
||||
}
|
||||
|
||||
if ( action && (action.status==='error' || action.status === 'warning') ) {
|
||||
rsssl_api.runLetsEncryptTest('installation_data').then( ( response ) => {
|
||||
if (response) {
|
||||
setInstallationData(response.output);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}, [action]);
|
||||
|
||||
|
||||
const handleCopyAction = (type) => {
|
||||
let success;
|
||||
let data = document.querySelector('.rsssl-'+type).innerText;
|
||||
|
||||
const el = document.createElement('textarea');
|
||||
el.value = data; //str is your string to copy
|
||||
document.body.appendChild(el);
|
||||
el.select();
|
||||
try {
|
||||
success = document.execCommand("copy");
|
||||
} catch (e) {
|
||||
success = false;
|
||||
}
|
||||
document.body.removeChild(el);
|
||||
const notice = dispatch('core/notices').createNotice(
|
||||
'success',
|
||||
__( 'Copied!', 'really-simple-ssl' ),
|
||||
{
|
||||
__unstableHTML: true,
|
||||
id: 'rsssl_copied_data',
|
||||
type: 'snackbar',
|
||||
isDismissible: true,
|
||||
}
|
||||
).then(sleeper(3000)).then(( response ) => {
|
||||
dispatch('core/notices').removeNotice('rsssl_copied_data');
|
||||
});
|
||||
}
|
||||
|
||||
if ( !action ) {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
|
||||
if (!installationData) {
|
||||
return (<></>);
|
||||
}
|
||||
return (
|
||||
<div className="rsssl-test-results">
|
||||
{ !installationData.generated_by_rsssl && <>{__("The certificate is not generated by Really Simple Security, so there are no installation files here","really-simple-ssl")}</>}
|
||||
|
||||
{ installationData.generated_by_rsssl && action.status === 'warning' &&
|
||||
<>
|
||||
<h4>{ __("Next step", "really-simple-ssl") }</h4>
|
||||
<div className="rsssl-template-intro">{ __("Install your certificate.", "really-simple-ssl")}</div>
|
||||
<h4>{ __("Certificate (CRT)", "really-simple-ssl") }</h4>
|
||||
<div className="rsssl-certificate-data rsssl-certificate" id="rsssl-certificate">{installationData.certificate_content}</div>
|
||||
<a href={installationData.download_url+"&type=certificate"} className="button button-secondary">{ __("Download", "really-simple-ssl")}</a>
|
||||
<button type="button" onClick={(e) => handleCopyAction('certificate')} className="button button-primary">{ __("Copy content", "really-simple-ssl")}</button>
|
||||
|
||||
<h4>{ __("Private Key (KEY)", "really-simple-ssl") }</h4>
|
||||
<div className="rsssl-certificate-data rsssl-key" id="rsssl-key">{installationData.key_content}</div>
|
||||
<a href={installationData.download_url+"&type=private_key"} className="button button-secondary">{ __("Download", "really-simple-ssl")}</a>
|
||||
<button type="button" className="button button-primary" onClick={(e) => handleCopyAction('key')} >{ __("Copy content", "really-simple-ssl")}</button>
|
||||
<h4>{ __("Certificate Authority Bundle (CABUNDLE)", "really-simple-ssl") }</h4>
|
||||
<div className="rsssl-certificate-data rsssl-cabundle" id="rsssl-cabundle">{installationData.ca_bundle_content}</div>
|
||||
<a href={installationData.download_url+"&type=intermediate"} className="button button-secondary">{ __("Download", "really-simple-ssl")}</a>
|
||||
<button type="button" className="button button-primary" onClick={(e) => handleCopyAction('cabundle')} >{ __("Copy content", "really-simple-ssl")}</button>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Installation;
|
||||
@@ -0,0 +1,311 @@
|
||||
import {useEffect, useRef} from "@wordpress/element";
|
||||
import * as rsssl_api from "../utils/api";
|
||||
import sleeper from "../utils/sleeper";
|
||||
import Directories from "./Directories";
|
||||
import DnsVerification from "./DnsVerification";
|
||||
import Generation from "./Generation";
|
||||
import Activate from "./Activate";
|
||||
import Installation from "./Installation";
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import Icon from "../utils/Icon";
|
||||
import useFields from "../Settings/FieldsData";
|
||||
import useLetsEncryptData from "./letsEncryptData";
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
const LetsEncrypt = ({field}) => {
|
||||
const {handleNextButtonDisabled, getFieldValue} = useFields();
|
||||
const {setSwitchButtonDisabled, actionsList, setActionsList, setActionsListItem, setActionsListProperty, actionIndex, setActionIndex, attemptCount, setAttemptCount, progress, setProgress, refreshTests, setRefreshTests} = useLetsEncryptData();
|
||||
const sleep = useRef(1000);
|
||||
const intervalId = useRef(false);
|
||||
const previousActionIndex = useRef(-1);
|
||||
const maxIndex = useRef(1);
|
||||
const refProgress = useRef(0);
|
||||
const lastAction = useRef({});
|
||||
|
||||
useEffect(() => {
|
||||
reset();
|
||||
}, [field.id])
|
||||
|
||||
useEffect(() => {
|
||||
setSwitchButtonDisabled(true);
|
||||
}, []);
|
||||
|
||||
const getActions = () => {
|
||||
let propActions = field.actions;
|
||||
if ( field.id==='generation' ) {
|
||||
propActions = adjustActionsForDNS(propActions);
|
||||
}
|
||||
|
||||
maxIndex.current = propActions.length;
|
||||
return propActions;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
handleNextButtonDisabled(false);
|
||||
if ( actionsList.length>0 && actionIndex===-1){
|
||||
setActionIndex(0);
|
||||
runTest(0, 0);
|
||||
}
|
||||
return () => {
|
||||
// Perform any cleanup logic here if needed
|
||||
// For example, you can cancel any ongoing asynchronous tasks or subscriptions
|
||||
};
|
||||
}, [actionsList])
|
||||
|
||||
useEffect(() => {
|
||||
}, [actionIndex, maxIndex.current]);
|
||||
|
||||
const startInterval = () => {
|
||||
intervalId.current = setInterval(() => {
|
||||
if (refProgress.current<100) {
|
||||
setProgress(refProgress.current + 0.2);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
previousActionIndex.current = actionIndex;
|
||||
setProgress( ( 100 / maxIndex.current ) * (actionIndex));
|
||||
|
||||
//ensure that progress does not get to 100 when retries are still running
|
||||
let currentAction = actionsList[actionIndex];
|
||||
if ( currentAction && currentAction.do==='retry' && attemptCount>1 ){
|
||||
setProgress(90);
|
||||
}
|
||||
|
||||
}, [actionIndex ])
|
||||
|
||||
useEffect (() => {
|
||||
refProgress.current = progress;
|
||||
},[progress])
|
||||
|
||||
useEffect(() => {
|
||||
if ( refreshTests ){
|
||||
setRefreshTests(false);
|
||||
reset();
|
||||
actionsList.forEach(function(action,i){
|
||||
setActionsListProperty(i, 'status', 'inactive');
|
||||
});
|
||||
}
|
||||
}, [refreshTests ])
|
||||
|
||||
const statuses = {
|
||||
'inactive': {
|
||||
'icon': 'circle-times',
|
||||
'color': 'grey',
|
||||
},
|
||||
'warning': {
|
||||
'icon': 'circle-times',
|
||||
'color': 'orange',
|
||||
},
|
||||
'error': {
|
||||
'icon': 'circle-times',
|
||||
'color': 'red',
|
||||
},
|
||||
'success': {
|
||||
'icon': 'circle-check',
|
||||
'color': 'green',
|
||||
},
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setSwitchButtonDisabled(true);
|
||||
// handleNextButtonDisabled(true);
|
||||
setActionsList(getActions());
|
||||
setProgress(0);
|
||||
refProgress.current = 0;
|
||||
setActionIndex(-1);
|
||||
previousActionIndex.current = -1;
|
||||
}
|
||||
|
||||
const adjustActionsForDNS = (actions) => {
|
||||
//find verification_type
|
||||
let verification_type = getFieldValue('verification_type');
|
||||
if ( !verification_type ) verification_type = 'dir';
|
||||
|
||||
if ( verification_type==='dns' ) {
|
||||
//check if dns verification already is added
|
||||
let dnsVerificationAdded = false;
|
||||
actions.forEach(function(action, i) {
|
||||
if (action.action==="verify_dns"){
|
||||
dnsVerificationAdded = true;
|
||||
}
|
||||
});
|
||||
|
||||
//find bundle index
|
||||
let create_bundle_index = -1;
|
||||
actions.forEach(function(action, i) {
|
||||
if (action.action==="create_bundle_or_renew"){
|
||||
create_bundle_index = i;
|
||||
}
|
||||
});
|
||||
|
||||
if (!dnsVerificationAdded && create_bundle_index>0) {
|
||||
//store create bundle action
|
||||
let actionsCopy = [...actions];
|
||||
let createBundleAction = actionsCopy[create_bundle_index];
|
||||
//overwrite create bundle action
|
||||
let newAction = {};
|
||||
newAction.action = 'verify_dns';
|
||||
newAction.description = __("Verifying DNS records...", "really-simple-ssl");
|
||||
newAction.attempts = 2;
|
||||
actionsCopy[create_bundle_index] = newAction;
|
||||
actionsCopy.push(createBundleAction);
|
||||
actions = actionsCopy;
|
||||
}
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
const processTestResult = async (action, newActionIndex) => {
|
||||
// clearInterval(intervalId.current);
|
||||
|
||||
if ( action.status==='success' ) {
|
||||
setAttemptCount(0);
|
||||
} else {
|
||||
if (!Number.isInteger(action.attemptCount)) {
|
||||
setAttemptCount(0);
|
||||
}
|
||||
//ensure attemptCount is an integer
|
||||
setAttemptCount( parseInt(attemptCount) + 1 );
|
||||
}
|
||||
|
||||
//used for dns verification actions
|
||||
let event = new CustomEvent('rsssl_le_response', { detail: action });
|
||||
document.dispatchEvent(event);
|
||||
//if all tests are finished with success
|
||||
//finalize happens when halfway through our tests it's finished. We can skip all others.
|
||||
if ( action.do === 'finalize' ) {
|
||||
actionsList.forEach(function(action,i){
|
||||
if (i>newActionIndex) {
|
||||
setActionsListProperty(i, 'hide', true);
|
||||
}
|
||||
});
|
||||
setActionIndex(maxIndex.current+1);
|
||||
setSwitchButtonDisabled(false);
|
||||
// handleNextButtonDisabled(false);
|
||||
} else if ( action.do === 'continue' || action.do === 'skip' ) {
|
||||
//new action, so reset the attempts count
|
||||
setAttemptCount(1);
|
||||
//skip: drop previous completely, skip to next.
|
||||
if ( action.do === 'skip' ) {
|
||||
setActionsListProperty(newActionIndex, 'hide', true);
|
||||
}
|
||||
//move to next action, but not if we're already on the max
|
||||
if ( maxIndex.current-1 > newActionIndex) {
|
||||
setActionIndex(newActionIndex+1);
|
||||
await runTest(newActionIndex+1);
|
||||
} else {
|
||||
setActionIndex(newActionIndex+1);
|
||||
setSwitchButtonDisabled(false);
|
||||
// handleNextButtonDisabled(false);
|
||||
}
|
||||
} else if (action.do === 'retry' ) {
|
||||
if ( attemptCount >= action.attempts ) {
|
||||
setSwitchButtonDisabled(false);
|
||||
setActionIndex(maxIndex.current);
|
||||
} else {
|
||||
setSwitchButtonDisabled(false);
|
||||
setActionIndex(newActionIndex);
|
||||
await runTest(newActionIndex);
|
||||
}
|
||||
} else if ( action.do === 'stop' ){
|
||||
setSwitchButtonDisabled(false);
|
||||
setActionIndex(maxIndex.current);
|
||||
}
|
||||
}
|
||||
|
||||
const runTest = async (newActionIndex) => {
|
||||
let currentAction = {...actionsList[newActionIndex]};
|
||||
if (!currentAction) return;
|
||||
let test = currentAction.action;
|
||||
const startTime = new Date();
|
||||
await rsssl_api.runLetsEncryptTest(test, field.id ).then( ( response ) => {
|
||||
const endTime = new Date();
|
||||
let timeDiff = endTime - startTime; //in ms
|
||||
const elapsedTime = Math.round(timeDiff);
|
||||
currentAction.status = response.status ? response.status : 'inactive';
|
||||
currentAction.hide = false;
|
||||
currentAction.description = response.message;
|
||||
currentAction.do = response.action;
|
||||
currentAction.output = response.output ? response.output : false;
|
||||
sleep.current = 500;
|
||||
if (elapsedTime<1500) {
|
||||
sleep.current = 1500-elapsedTime;
|
||||
}
|
||||
setActionsListItem(newActionIndex, currentAction);
|
||||
}).then(sleeper(sleep.current)).then( () => {
|
||||
processTestResult(currentAction, newActionIndex);
|
||||
});
|
||||
}
|
||||
|
||||
const getStyles = (newProgress) => {
|
||||
return Object.assign(
|
||||
{},
|
||||
{width: newProgress+"%"},
|
||||
);
|
||||
}
|
||||
|
||||
const getStatusIcon = (action) => {
|
||||
if (!statuses.hasOwnProperty(action.status)) {
|
||||
return statuses['inactive'].icon;
|
||||
}
|
||||
return statuses[action.status].icon
|
||||
}
|
||||
|
||||
const getStatusColor = (action) => {
|
||||
if (!statuses.hasOwnProperty(action.status)) {
|
||||
return statuses['inactive'].color;
|
||||
}
|
||||
return statuses[action.status].color;
|
||||
}
|
||||
|
||||
if ( !field.actions ) {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
let progressCopy = progress;
|
||||
if (maxIndex.current === actionIndex+1 ){
|
||||
progressCopy = 100;
|
||||
}
|
||||
|
||||
//filter out skipped actions
|
||||
let actionsOutput = actionsList.filter(action => action.hide !== true);
|
||||
//ensure the sub components have an action to look at, also if the action has been dropped after last test.
|
||||
let action = actionsList[actionIndex];
|
||||
if (action){
|
||||
lastAction.current = action;
|
||||
} else {
|
||||
action = lastAction.current;
|
||||
}
|
||||
let progressBarColor = action.status==='error' ? 'rsssl-orange' : '';
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-lets-encrypt-tests">
|
||||
<div className="rsssl-progress-bar"><div className="rsssl-progress"><div className={'rsssl-bar ' + progressBarColor} style={getStyles(progressCopy)}></div></div></div>
|
||||
<div className="rsssl_letsencrypt_container rsssl-progress-container field-group">
|
||||
<ul>
|
||||
{actionsOutput.map((action, i) =>
|
||||
<li key={"action-"+i}>
|
||||
<Icon name = {getStatusIcon(action)} color = {getStatusColor(action)} />
|
||||
{action.do==='retry' && attemptCount >=1 && <>{__("Attempt %s.", "really-simple-ssl").replace('%s', attemptCount)} </>}
|
||||
|
||||
<span dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(action.description) }}></span> {/* nosemgrep: react-dangerouslysetinnerhtml */}
|
||||
</li>
|
||||
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
{field.id === 'directories' && <Directories field={field} action={action}/> }
|
||||
{field.id === 'dns-verification' && <DnsVerification field={field} action={action}/> }
|
||||
{field.id === 'generation' && <Generation field={field} action={action}/> }
|
||||
{field.id === 'installation' && <Installation field={field} action={action}/> }
|
||||
{field.id === 'activate' && <Activate field={field} action={action}/> }
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LetsEncrypt;
|
||||
@@ -0,0 +1,58 @@
|
||||
import {create} from 'zustand';
|
||||
import produce from 'immer';
|
||||
import * as rsssl_api from "../utils/api";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import sleeper from "../utils/sleeper";
|
||||
import {dispatch} from '@wordpress/data';
|
||||
const useLetsEncryptData = create(( set, get ) => ({
|
||||
actionIndex:-1,
|
||||
progress:0,
|
||||
attemptCount:0,
|
||||
refreshTests:false,
|
||||
actionsList:[],
|
||||
updateVerificationType: async (verificationType) => {
|
||||
await rsssl_api.runLetsEncryptTest('update_verification_type', verificationType).then((response) => {
|
||||
let msg = verificationType==='dir' ? __('Switched to Directory', 'really-simple-ssl') : __('Switched to DNS', 'really-simple-ssl');
|
||||
const notice = dispatch('core/notices').createNotice(
|
||||
'success',
|
||||
msg,
|
||||
{
|
||||
__unstableHTML: true,
|
||||
id: 'rsssl_switched_to_dns',
|
||||
type: 'snackbar',
|
||||
isDismissible: true,
|
||||
}
|
||||
).then(sleeper(3000)).then((response) => {
|
||||
dispatch('core/notices').removeNotice('rsssl_switched_to_dns');
|
||||
});
|
||||
});
|
||||
},
|
||||
setAttemptCount: (attemptCount) => {set(state => ({ attemptCount }))},
|
||||
setProgress: (progress) => {set(state => ({ progress }))},
|
||||
setActionsList: (actionsList) => {set(state => ({ actionsList }))},
|
||||
setActionsListItem: (index, action) => {
|
||||
set(
|
||||
produce((state) => {
|
||||
state.actionsList[index] = action;
|
||||
})
|
||||
)
|
||||
},
|
||||
setActionsListProperty: (index, property, value) => {
|
||||
set(
|
||||
produce((state) => {
|
||||
//first, check if the actionsList has index and property
|
||||
if (typeof state.actionsList[index] === 'undefined' || typeof state.actionsList[index][property]) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.actionsList[index][property] = value;
|
||||
})
|
||||
)
|
||||
},
|
||||
setRefreshTests: (refreshTests) => {set(state => ({ refreshTests }))},
|
||||
setActionIndex: (actionIndex) => {set(state => ({ actionIndex }))},
|
||||
switchButtonDisabled:false,
|
||||
setSwitchButtonDisabled: (switchButtonDisabled) => {set(state => ({ switchButtonDisabled }))},
|
||||
}));
|
||||
export default useLetsEncryptData;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import MenuPlaceholder from '../Placeholder/MenuPlaceholder';
|
||||
import MenuItem from './MenuItem';
|
||||
import useMenu from "./MenuData";
|
||||
/**
|
||||
* Menu block, rendering the entire menu
|
||||
*/
|
||||
const Menu = () => {
|
||||
const {subMenu, subMenuLoaded} = useMenu();
|
||||
|
||||
if ( !subMenuLoaded ) {
|
||||
return(
|
||||
<MenuPlaceholder />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rsssl-wizard-menu rsssl-grid-item">
|
||||
<div className="rsssl-grid-item-header">
|
||||
<h1 className="rsssl-h4">{subMenu.title}</h1>
|
||||
</div>
|
||||
<div className="rsssl-grid-item-content">
|
||||
<div className="rsssl-wizard-menu-items">
|
||||
{ subMenu.menu_items.map((menuItem, i) => <MenuItem key={"menuItem-"+i} menuItem={menuItem} isMainMenu={true} /> ) }
|
||||
</div>
|
||||
</div>
|
||||
<div className="rsssl-grid-item-footer">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Menu;
|
||||
@@ -0,0 +1,248 @@
|
||||
import {create} from 'zustand';
|
||||
import getAnchor from "../utils/getAnchor";
|
||||
|
||||
const useMenu = create(( set, get ) => ({
|
||||
menu: [],
|
||||
subMenuLoaded:false,
|
||||
previousMenuItem:false,
|
||||
nextMenuItem:false,
|
||||
selectedMainMenuItem:false,
|
||||
selectedSubMenuItem:false,
|
||||
selectedFilter: false,
|
||||
activeGroupId: false,
|
||||
hasPremiumItems:false,
|
||||
subMenu:{title:' ',menu_items:[]},
|
||||
setSelectedSubMenuItem: async (selectedSubMenuItem) => {
|
||||
let selectedMainMenuItem = getMainMenuForSubMenu(selectedSubMenuItem);
|
||||
set(state => ({ selectedSubMenuItem,selectedMainMenuItem }))
|
||||
// window.location.href=rsssl_settings.dashboard_url+'#'+selectedMainMenuItem+'/'+selectedSubMenuItem;
|
||||
window.location.hash = selectedMainMenuItem+'/'+selectedSubMenuItem;
|
||||
},
|
||||
setSelectedMainMenuItem: (selectedMainMenuItem) => {
|
||||
set(state => ({ selectedMainMenuItem }))
|
||||
// window.location.href=rsssl_settings.dashboard_url+'#'+selectedMainMenuItem;
|
||||
window.location.hash = selectedMainMenuItem;
|
||||
},
|
||||
//we need to get the main menu item directly from the anchor, otherwise we have to wait for the menu to load in page.js
|
||||
fetchSelectedMainMenuItem: () => {
|
||||
let selectedMainMenuItem = getAnchor('main') || 'dashboard';
|
||||
set((state) => ({selectedMainMenuItem: selectedMainMenuItem}));
|
||||
},
|
||||
fetchSelectedSubMenuItem: async () => {
|
||||
let selectedSubMenuItem = getAnchor('menu') || 'general';
|
||||
set((state) => ({selectedSubMenuItem: selectedSubMenuItem}));
|
||||
},
|
||||
fetchMenuData: (fields) => {
|
||||
let menu = rsssl_settings.menu;
|
||||
menu = Object.values(menu);
|
||||
const selectedMainMenuItem = getAnchor('main') || 'dashboard';
|
||||
menu = menu.filter( item => !item.default_hidden || selectedMainMenuItem===item.id);
|
||||
|
||||
if ( typeof fields !== 'undefined' ) {
|
||||
let subMenu = getSubMenu(menu, selectedMainMenuItem);
|
||||
const selectedSubMenuItem = getSelectedSubMenuItem(subMenu, fields);
|
||||
subMenu.menu_items = dropEmptyMenuItems(subMenu.menu_items, fields, selectedSubMenuItem);
|
||||
const { nextMenuItem, previousMenuItem } = getPreviousAndNextMenuItems(menu, selectedSubMenuItem, fields);
|
||||
const hasPremiumItems = subMenu.menu_items.filter((item) => {return (item.premium===true)}).length>0;
|
||||
set((state) => ({subMenuLoaded:true, menu: menu, nextMenuItem:nextMenuItem, previousMenuItem:previousMenuItem, selectedMainMenuItem: selectedMainMenuItem, selectedSubMenuItem:selectedSubMenuItem, subMenu: subMenu, hasPremiumItems: hasPremiumItems}));
|
||||
} else {
|
||||
set((state) => ({menu: menu, selectedMainMenuItem: selectedMainMenuItem}));
|
||||
|
||||
}
|
||||
},
|
||||
getDefaultSubMenuItem: async (fields) => {
|
||||
let subMenuLoaded = get().subMenuLoaded;
|
||||
if (!subMenuLoaded){
|
||||
await get().fetchMenuData(fields);
|
||||
}
|
||||
let subMenu = get().subMenu;
|
||||
let fallBackMenuItem = subMenuLoaded && subMenu.hasOwnProperty(0) ? subMenu[0].id : 'general';
|
||||
let anchor = getAnchor('menu');
|
||||
let foundAnchorInMenu = false;
|
||||
//check if this anchor actually exists in our current submenu. If not, clear it
|
||||
for (const key in this.menu.menu_items) {
|
||||
if ( subMenu.hasOwnProperty(key) && subMenu[key].id === anchor ){
|
||||
foundAnchorInMenu=true;
|
||||
}
|
||||
}
|
||||
if ( !foundAnchorInMenu ) anchor = false;
|
||||
return anchor ? anchor : fallBackMenuItem;
|
||||
}
|
||||
}));
|
||||
export default useMenu;
|
||||
|
||||
|
||||
// Parses menu items and nested items in single array
|
||||
const menuItemParser = (parsedMenuItems, menuItems, fields) => {
|
||||
menuItems.forEach((menuItem) => {
|
||||
if( menuItem.visible ) {
|
||||
parsedMenuItems.push(menuItem.id);
|
||||
if( menuItem.hasOwnProperty('menu_items') ) {
|
||||
menuItem.menu_items = dropEmptyMenuItems(menuItem.menu_items, fields );
|
||||
menuItemParser(parsedMenuItems, menuItem.menu_items, fields);
|
||||
}
|
||||
}
|
||||
});
|
||||
return parsedMenuItems;
|
||||
}
|
||||
|
||||
const getPreviousAndNextMenuItems = (menu, selectedSubMenuItem, fields) => {
|
||||
let previousMenuItem;
|
||||
let nextMenuItem;
|
||||
const parsedMenuItems = [];
|
||||
menuItemParser(parsedMenuItems, menu, fields);
|
||||
// Finds current menu item index
|
||||
const currentMenuItemIndex = parsedMenuItems.findIndex((menuItem) => menuItem === selectedSubMenuItem);
|
||||
if( currentMenuItemIndex !== -1 ) {
|
||||
previousMenuItem = parsedMenuItems[ currentMenuItemIndex === 0 ? '' : currentMenuItemIndex - 1];
|
||||
//if the previous menu item has a submenu, we should move one more back, because it will select the current sub otherwise.
|
||||
const previousMenuHasSubMenu = getMenuItemByName(previousMenuItem, menu).hasOwnProperty('menu_items');
|
||||
if (previousMenuHasSubMenu) {
|
||||
previousMenuItem = parsedMenuItems[ currentMenuItemIndex === 0 ? '' : currentMenuItemIndex - 2]
|
||||
}
|
||||
nextMenuItem = parsedMenuItems[ currentMenuItemIndex === parsedMenuItems.length - 1 ? '' : currentMenuItemIndex + 1];
|
||||
previousMenuItem = previousMenuItem ? previousMenuItem : parsedMenuItems[0];
|
||||
nextMenuItem = nextMenuItem ? nextMenuItem : parsedMenuItems[parsedMenuItems.length - 1]
|
||||
}
|
||||
return { nextMenuItem, previousMenuItem };
|
||||
}
|
||||
|
||||
const dropEmptyMenuItems = (menuItems, fields) => {
|
||||
|
||||
if (!Array.isArray(fields)) {
|
||||
return menuItems; // return the original menuItems unchanged
|
||||
}
|
||||
|
||||
const newMenuItems = menuItems;
|
||||
for (const [index, menuItem] of menuItems.entries()) {
|
||||
let menuItemFields = fields.filter((field) => {
|
||||
return (field.menu_id === menuItem.id )
|
||||
});
|
||||
|
||||
menuItemFields = menuItemFields.filter((field) => {
|
||||
return ( field.visible )
|
||||
});
|
||||
if ( menuItemFields.length === 0 && !menuItem.hasOwnProperty('menu_items') ) {
|
||||
if (typeof newMenuItems[index] === 'object' && newMenuItems[index] !== null) {
|
||||
newMenuItems[index].visible = false;
|
||||
}
|
||||
} else {
|
||||
if (typeof newMenuItems[index] === 'object' && newMenuItems[index] !== null) {
|
||||
newMenuItems[index].visible = true;
|
||||
}
|
||||
if( menuItem.hasOwnProperty('menu_items') ) {
|
||||
newMenuItems[index].menu_items = dropEmptyMenuItems(menuItem.menu_items, fields);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
return newMenuItems;
|
||||
}
|
||||
|
||||
/*
|
||||
* filter sidebar menu from complete menu structure
|
||||
*/
|
||||
const getSubMenu = (menu, selectedMainMenuItem) => {
|
||||
|
||||
let subMenu = [];
|
||||
for (const key in menu) {
|
||||
if ( menu.hasOwnProperty(key) && menu[key].id === selectedMainMenuItem ){
|
||||
subMenu = menu[key];
|
||||
}
|
||||
}
|
||||
|
||||
subMenu = addVisibleToMenuItems(subMenu);
|
||||
return subMenu;
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the main menu item for a submenu item
|
||||
*/
|
||||
const getMainMenuForSubMenu = (findMenuItem) => {
|
||||
let menu = rsssl_settings.menu;
|
||||
for (const mainKey in menu) {
|
||||
let mainMenuItem = menu[mainKey];
|
||||
if (mainMenuItem.id===findMenuItem) {
|
||||
return mainMenuItem.id;
|
||||
}
|
||||
if (mainMenuItem.menu_items){
|
||||
for (const subKey in mainMenuItem.menu_items) {
|
||||
let subMenuItem = mainMenuItem.menu_items[subKey];
|
||||
if (subMenuItem.id===findMenuItem) {
|
||||
return mainMenuItem.id;
|
||||
}
|
||||
if (subMenuItem.menu_items){
|
||||
for (const sub2Key in subMenuItem.menu_items) {
|
||||
let sub2MenuItem = subMenuItem.menu_items[sub2Key];
|
||||
if (sub2MenuItem.id===findMenuItem) {
|
||||
return mainMenuItem.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current selected menu item based on the hash, selecting subitems if the main one is empty.
|
||||
*/
|
||||
const getSelectedSubMenuItem = (subMenu, fields) => {
|
||||
let fallBackMenuItem = subMenu && subMenu.menu_items.hasOwnProperty(0) ? subMenu.menu_items[0].id : 'general';
|
||||
let foundAnchorInMenu;
|
||||
|
||||
//get flat array of menu items
|
||||
let parsedMenuItems = menuItemParser([], subMenu.menu_items);
|
||||
let anchor = getAnchor('menu');
|
||||
//check if this anchor actually exists in our current submenu. If not, clear it
|
||||
foundAnchorInMenu = parsedMenuItems.filter(menu_item => menu_item === anchor);
|
||||
if ( !foundAnchorInMenu ) {
|
||||
anchor = false;
|
||||
}
|
||||
let selectedMenuItem = anchor ? anchor : fallBackMenuItem;
|
||||
//check if menu item has fields. If not, try a subitem
|
||||
let fieldsInMenu = fields.filter(field => field.menu_id === selectedMenuItem);
|
||||
if ( fieldsInMenu.length===0 ) {
|
||||
//look up the current menu item
|
||||
let menuItem = getMenuItemByName(selectedMenuItem, subMenu.menu_items);
|
||||
if (menuItem && menuItem.menu_items && menuItem.menu_items.hasOwnProperty(0)) {
|
||||
selectedMenuItem = menuItem.menu_items[0].id;
|
||||
}
|
||||
}
|
||||
return selectedMenuItem;
|
||||
}
|
||||
|
||||
//Get a menu item by name from the menu array
|
||||
const getMenuItemByName = (name, menuItems) => {
|
||||
for (const key in menuItems ){
|
||||
let menuItem = menuItems[key];
|
||||
if ( menuItem.id === name ) {
|
||||
return menuItem;
|
||||
}
|
||||
if ( menuItem.menu_items ) {
|
||||
let found = getMenuItemByName(name, menuItem.menu_items);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const addVisibleToMenuItems = (menu) => {
|
||||
|
||||
let newMenuItems = Array.isArray(menu.menu_items) ? menu.menu_items : Object.values(menu.menu_items);
|
||||
|
||||
for (let [index, menuItem] of newMenuItems.entries()) {
|
||||
if (typeof menuItem === 'object' && menuItem !== null) {
|
||||
menuItem.visible = true;
|
||||
if (menuItem.hasOwnProperty('menu_items')) {
|
||||
menuItem = addVisibleToMenuItems(menuItem);
|
||||
}
|
||||
newMenuItems[index] = menuItem;
|
||||
}
|
||||
}
|
||||
menu.menu_items = newMenuItems;
|
||||
menu.visible = true;
|
||||
return menu;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import useMenu from "./MenuData";
|
||||
|
||||
const MenuItem = (props) => {
|
||||
const {selectedSubMenuItem, selectedMainMenuItem, subMenu, menu} = useMenu();
|
||||
const menuIsSelected = isSelectedMenuItem(selectedSubMenuItem, props.menuItem);
|
||||
|
||||
const ensureArray = (data) => {
|
||||
return Array.isArray(data) ? data : [data];
|
||||
}
|
||||
|
||||
let menuClass = menuIsSelected ? ' rsssl-active' : '';
|
||||
menuClass += props.menuItem.featured ? ' rsssl-featured' : '';
|
||||
menuClass += props.menuItem.new ? ' rsssl-new' : '';
|
||||
menuClass += props.menuItem.premium && !rsssl_settings.pro_plugin_active ? ' rsssl-premium' : '';
|
||||
let menuLink = props.menuItem.directLink || '#'+selectedMainMenuItem+'/'+props.menuItem.id;
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.menuItem.visible && (
|
||||
<>
|
||||
{props.isMainMenu ? (
|
||||
<div className="rsssl-main-menu">
|
||||
<div className={"rsssl-menu-item" + menuClass}>
|
||||
<a href={menuLink}>
|
||||
<span>{props.menuItem.title}</span>
|
||||
{props.menuItem.featured && <span className='rsssl-menu-item-beta-pill'>{__('Beta', 'really-simple-ssl')}</span>}
|
||||
{props.menuItem.new && <span className='rsssl-menu-item-new-pill'>{__('New', 'really-simple-ssl')}</span>}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={"rsssl-menu-item" + menuClass}>
|
||||
<a href={menuLink}>
|
||||
<span>{props.menuItem.title}</span>
|
||||
{props.menuItem.featured && <span className='rsssl-menu-item-beta-pill'>{__('Beta', 'really-simple-ssl')}</span>}
|
||||
{props.menuItem.new && <span className='rsssl-menu-item-new-pill'>{__('New', 'really-simple-ssl')}</span>}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.menuItem.menu_items && menuIsSelected && (
|
||||
<div className="rsssl-submenu-item">
|
||||
{ensureArray(props.menuItem.menu_items).map((subMenuItem, i) => (
|
||||
subMenuItem.visible && <MenuItem key={"submenuItem" + i} menuItem={subMenuItem} isMainMenu={false} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default MenuItem
|
||||
|
||||
/**
|
||||
* Utility function to check if selected menu item is the current menu item or a child of the current menu item
|
||||
* @param selectedSubMenuItem
|
||||
* @param menuItem
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isSelectedMenuItem = (selectedSubMenuItem, menuItem) => {
|
||||
if (selectedSubMenuItem === menuItem.id) {
|
||||
return true;
|
||||
}
|
||||
if (menuItem.menu_items) {
|
||||
for (const item of menuItem.menu_items) {
|
||||
if (item.id === selectedSubMenuItem) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import * as rsssl_api from "../utils/api";
|
||||
import Icon from "../utils/Icon";
|
||||
import useModal from "./ModalData";
|
||||
import {useState} from '@wordpress/element';
|
||||
|
||||
const Modal = (props) => {
|
||||
const {handleModal, modalData, setModalData, showModal, setIgnoredItemId, setFixedItemId, item} = useModal();
|
||||
const [buttonsDisabled, setButtonsDisabled] = useState(false);
|
||||
|
||||
const dismissModal = () => {
|
||||
handleModal(false, null, null);
|
||||
}
|
||||
|
||||
const handleFix = (e, type) => {
|
||||
//set to disabled
|
||||
let action = modalData.action;
|
||||
setButtonsDisabled(true);
|
||||
rsssl_api.runTest(action, 'refresh', modalData ).then( ( response ) => {
|
||||
let data = {...modalData};
|
||||
data.description = response.msg;
|
||||
data.subtitle = '';
|
||||
setModalData(data);
|
||||
setButtonsDisabled(false);
|
||||
if (response.success) {
|
||||
if (type==='ignore' && item !==false ) {
|
||||
setIgnoredItemId(item.id);
|
||||
} else {
|
||||
setFixedItemId(item.id);
|
||||
}
|
||||
handleModal(false, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!showModal) {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
let disabled = buttonsDisabled ? 'disabled' : '';
|
||||
let description = modalData.description;
|
||||
if ( !Array.isArray(description) ) {
|
||||
description = [description];
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="rsssl-modal-backdrop" onClick={ (e) => dismissModal(e) }> </div>
|
||||
<div className="rsssl-modal" id="{id}">
|
||||
<div className="rsssl-modal-header">
|
||||
<h2 className="modal-title">
|
||||
{modalData.title}
|
||||
</h2>
|
||||
<button type="button" className="rsssl-modal-close" data-dismiss="modal" aria-label="Close" onClick={ (e) => dismissModal(e) }>
|
||||
<Icon name='times' />
|
||||
</button>
|
||||
</div>
|
||||
<div className="rsssl-modal-content">
|
||||
{ modalData.subtitle && <div className="rsssl-modal-subtitle">{modalData.subtitle}</div>}
|
||||
{ Array.isArray(description) && description.map((s, i) => <div key={"modalDescription-"+i} className="rsssl-modal-description">{s}</div>) }
|
||||
</div>
|
||||
<div className="rsssl-modal-footer">
|
||||
{ modalData.edit && <a href={modalData.edit} target="_blank" rel="noopener noreferrer" className="button button-secondary">{__("Edit", "really-simple-ssl")}</a>}
|
||||
{ modalData.help && <a href={modalData.help} target="_blank" rel="noopener noreferrer" className="button rsssl-button-help">{__("Help", "really-simple-ssl")}</a>}
|
||||
{ (!modalData.ignored && modalData.action==='ignore_url') && <button disabled={disabled} className="button button-primary" onClick={ (e) => handleFix(e, 'ignore') }>{ __("Ignore", "really-simple-ssl")}</button>}
|
||||
{ modalData.action!=='ignore_url' && <button disabled={disabled} className="button button-primary" onClick={ (e) => handleFix(e, 'fix') }>{__("Fix", "really-simple-ssl")}</button> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Modal;
|
||||
@@ -0,0 +1,19 @@
|
||||
import useModal from "./ModalData";
|
||||
|
||||
/**
|
||||
* Button to open the modal
|
||||
* @param props
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const ModalControl = (props) => {
|
||||
const {handleModal} = useModal();
|
||||
const onClickHandler = () => {
|
||||
handleModal(true, props.modalData, props.item );
|
||||
}
|
||||
|
||||
return (
|
||||
<button className={"button button-" + props.btnStyle} onClick={ (e) => onClickHandler(e) }>{props.btnText}</button>
|
||||
)
|
||||
}
|
||||
export default ModalControl
|
||||
@@ -0,0 +1,28 @@
|
||||
import {create} from 'zustand';
|
||||
|
||||
const useModalData = create(( set, get ) => ({
|
||||
modalData: [],
|
||||
buttonsDisabled: false,
|
||||
showModal:false,
|
||||
ignoredItems:[],
|
||||
fixedItems:[],
|
||||
item:false,
|
||||
setIgnoredItemId: (ignoredItemId) => {
|
||||
let ignoredItems = get().ignoredItems;
|
||||
ignoredItems.push(ignoredItemId);
|
||||
set({ignoredItems: ignoredItems, });
|
||||
},
|
||||
setFixedItemId: (fixedItemId) => {
|
||||
let fixedItems = get().fixedItems;
|
||||
fixedItems.push(fixedItemId);
|
||||
set({fixedItems: fixedItems, });
|
||||
},
|
||||
handleModal: (showModal, modalData, item) => {
|
||||
set({showModal: showModal, modalData:modalData, item:item });
|
||||
},
|
||||
setModalData: (modalData) => {
|
||||
set({modalData:modalData });
|
||||
},
|
||||
}));
|
||||
|
||||
export default useModalData;
|
||||
@@ -0,0 +1,21 @@
|
||||
import useOnboardingData from "../OnboardingData";
|
||||
import {memo} from "@wordpress/element";
|
||||
|
||||
const CheckboxItem = ({item, disabled}) => {
|
||||
const {
|
||||
updateItemStatus,
|
||||
currentStep
|
||||
} = useOnboardingData();
|
||||
let { title, description, id, activated } = item;
|
||||
return (
|
||||
<li>
|
||||
<label className="rsssl-modal-checkbox-container">
|
||||
<input type="checkbox" disabled={disabled} checked={activated} value={id} id={id} onChange={(e) => updateItemStatus(currentStep.id, id, null, null, e.target.checked )}/>
|
||||
<span className="rsssl-checkmark"></span>
|
||||
</label>
|
||||
{title}
|
||||
{description && <> - {description}</>}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
export default memo(CheckboxItem)
|
||||
@@ -0,0 +1,66 @@
|
||||
import Icon from "../../utils/Icon";
|
||||
import {memo} from "@wordpress/element";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import useOnboardingData from "../OnboardingData";
|
||||
|
||||
const ListItem = ({item}) => {
|
||||
let { title, status, id } = item;
|
||||
const {
|
||||
overrideSSL,
|
||||
setOverrideSSL,
|
||||
certificateValid,
|
||||
} = useOnboardingData();
|
||||
const statuses = {
|
||||
'inactive': {
|
||||
'icon': 'info',
|
||||
'color': 'grey',
|
||||
},
|
||||
'warning': {
|
||||
'icon': 'circle-times',
|
||||
'color': 'orange',
|
||||
},
|
||||
'error': {
|
||||
'icon': 'circle-times',
|
||||
'color': 'red',
|
||||
},
|
||||
'success': {
|
||||
'icon': 'circle-check',
|
||||
'color': 'green',
|
||||
},
|
||||
'processing': {
|
||||
'icon': 'loading',
|
||||
'color': 'black',
|
||||
},
|
||||
};
|
||||
|
||||
const statusIcon = item.status!=='success' && item.current_action === 'none' ? 'empty' : statuses[status].icon;
|
||||
const statusColor = statuses[status].color;
|
||||
return (
|
||||
<>
|
||||
<li>
|
||||
<Icon name = {statusIcon} color = {statusColor} />
|
||||
{title}
|
||||
{ id==='certificate' && !certificateValid &&
|
||||
<>
|
||||
<a href="#" onClick={ (e) => refreshSSLStatus(e)}>
|
||||
{ __("Check again", "really-simple-ssl")}
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
</li>
|
||||
{ id==='certificate' && !certificateValid &&
|
||||
<li>
|
||||
<label className="rsssl-override-detection-toggle">
|
||||
<input
|
||||
onChange={ (e) => setOverrideSSL(e.target.checked)}
|
||||
type="checkbox"
|
||||
checked={overrideSSL} />
|
||||
{__("Override SSL detection.","really-simple-ssl")}
|
||||
</label>
|
||||
</li>
|
||||
}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
export default memo(ListItem)
|
||||
@@ -0,0 +1,13 @@
|
||||
import {memo} from "@wordpress/element";
|
||||
const PremiumItem = ({item}) => {
|
||||
let { title } = item;
|
||||
return (
|
||||
<li>
|
||||
<div className="rsssl-modal-premium-container">
|
||||
PRO
|
||||
</div>
|
||||
{title}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
export default memo(PremiumItem)
|
||||
@@ -0,0 +1,100 @@
|
||||
import { useEffect } from "@wordpress/element";
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import Icon from "../utils/Icon";
|
||||
import Placeholder from '../Placeholder/Placeholder';
|
||||
import useFields from "../Settings/FieldsData";
|
||||
import useOnboardingData from "./OnboardingData";
|
||||
import OnboardingControls from "./OnboardingControls";
|
||||
import StepEmail from "./Steps/StepEmail";
|
||||
import StepConfig from "./Steps/StepConfig";
|
||||
import StepLicense from "./Steps/StepLicense";
|
||||
import StepFeatures from "./Steps/StepFeatures";
|
||||
import StepPlugins from "./Steps/StepPlugins";
|
||||
import StepPro from "./Steps/StepPro";
|
||||
import './PremiumItem.scss';
|
||||
import './checkbox.scss';
|
||||
import './onboarding.scss';
|
||||
import DOMPurify from 'dompurify';
|
||||
const Onboarding = ({isModal}) => {
|
||||
const { fetchFieldsData, fieldsLoaded} = useFields();
|
||||
const {
|
||||
getSteps,
|
||||
error,
|
||||
networkwide,
|
||||
sslEnabled,
|
||||
dataLoaded,
|
||||
processing,
|
||||
currentStep,
|
||||
currentStepIndex,
|
||||
networkActivationStatus,
|
||||
networkProgress,
|
||||
activateSSLNetworkWide,
|
||||
emailVerified
|
||||
} = useOnboardingData();
|
||||
|
||||
// Single effect to initialize
|
||||
useEffect(() => {
|
||||
getSteps(false);
|
||||
}, []); // Empty dependency array
|
||||
|
||||
if (error){
|
||||
return (
|
||||
<Placeholder lines="3" error={error}></Placeholder>
|
||||
)
|
||||
}
|
||||
|
||||
let processingClass = '';
|
||||
|
||||
return (
|
||||
<>
|
||||
{ !dataLoaded &&
|
||||
<>
|
||||
<div className="rsssl-onboarding-placeholder">
|
||||
<ul>
|
||||
<li><Icon name = "loading" color = 'grey' />{__("Fetching next step...", "really-simple-ssl")}</li>
|
||||
</ul>
|
||||
<Placeholder lines="3" ></Placeholder>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{
|
||||
dataLoaded &&
|
||||
<div className={ processingClass+" rsssl-"+currentStep.id }>
|
||||
{ currentStep.id === 'activate_ssl' &&
|
||||
<>
|
||||
<StepConfig isModal={isModal}/>
|
||||
</>
|
||||
}
|
||||
{ currentStep.id === 'activate_license' &&
|
||||
<>
|
||||
<StepLicense />
|
||||
</>
|
||||
}
|
||||
{ currentStep.id === 'features'&&
|
||||
<>
|
||||
<StepFeatures />
|
||||
</>
|
||||
}
|
||||
{currentStep.id === 'email' && !emailVerified && (
|
||||
<StepEmail />
|
||||
)}
|
||||
{ currentStep.id === 'plugins' &&
|
||||
<>
|
||||
<StepPlugins />
|
||||
</>
|
||||
}
|
||||
{ currentStep.id === 'pro' &&
|
||||
<>
|
||||
<StepPro />
|
||||
</>
|
||||
}
|
||||
{ !isModal &&
|
||||
<OnboardingControls isModal={false}/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Onboarding;
|
||||
@@ -0,0 +1,237 @@
|
||||
import { Button } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import useMenu from "../Menu/MenuData";
|
||||
import useFields from "../Settings/FieldsData";
|
||||
import useOnboardingData from "./OnboardingData";
|
||||
import useProgress from "../Dashboard/Progress/ProgressData";
|
||||
import useRiskData from "../Settings/RiskConfiguration/RiskData";
|
||||
import useSslLabs from "../Dashboard/SslLabs/SslLabsData";
|
||||
import useLicense from "../Settings/License/LicenseData";
|
||||
|
||||
const OnboardingControls = ({isModal}) => {
|
||||
const { getProgressData} = useProgress();
|
||||
const { updateField, setChangedField, updateFieldsData, fetchFieldsData, saveFields, getFieldValue} = useFields();
|
||||
const { setSelectedMainMenuItem, selectedSubMenuItem} = useMenu();
|
||||
const { licenseStatus, toggleActivation } = useLicense();
|
||||
const {
|
||||
fetchFirstRun, fetchVulnerabilities
|
||||
} = useRiskData();
|
||||
const {
|
||||
setSslScanStatus,
|
||||
} = useSslLabs();
|
||||
const {
|
||||
dismissModal,
|
||||
activateSSL,
|
||||
certificateValid,
|
||||
setFooterStatus,
|
||||
networkwide,
|
||||
processing,
|
||||
setProcessing,
|
||||
steps,
|
||||
currentStepIndex,
|
||||
currentStep,
|
||||
setCurrentStepIndex,
|
||||
overrideSSL,
|
||||
email,
|
||||
saveEmail,
|
||||
pluginInstaller,
|
||||
} = useOnboardingData();
|
||||
|
||||
const goToDashboard = () => {
|
||||
if ( isModal ) {
|
||||
dismissModal(true);
|
||||
}
|
||||
setSelectedMainMenuItem('dashboard');
|
||||
}
|
||||
|
||||
const saveAndContinue = async () => {
|
||||
|
||||
let vulnerabilityDetectionEnabled = false;
|
||||
if (currentStep.id === 'features') {
|
||||
setCurrentStepIndex(currentStepIndex+1);
|
||||
setProcessing(true);
|
||||
//loop through all items of currentStep.items
|
||||
for (const item of currentStep.items){
|
||||
if ( item.id=== 'health_scan' && item.activated ) {
|
||||
setFooterStatus(__("Starting SSL health scan...", "really-simple-ssl") );
|
||||
setSslScanStatus('active');
|
||||
}
|
||||
|
||||
if ( ! item.premium || ! item.activated ) {
|
||||
for (const fieldId of Object.values(item.options)) {
|
||||
const value = item.value || item.activated;
|
||||
updateField(fieldId, value);
|
||||
setChangedField(fieldId, value);
|
||||
}
|
||||
}
|
||||
|
||||
if ( item.id === 'vulnerability_detection' ) {
|
||||
vulnerabilityDetectionEnabled = item.activated;
|
||||
}
|
||||
}
|
||||
setFooterStatus(__("Activating options...", "really-simple-ssl") );
|
||||
await saveFields(true, false);
|
||||
if (vulnerabilityDetectionEnabled) {
|
||||
setFooterStatus(__("Initializing vulnerability detection...", "really-simple-ssl") );
|
||||
await fetchFirstRun();
|
||||
setFooterStatus(__("Scanning for vulnerabilities...", "really-simple-ssl") );
|
||||
await fetchVulnerabilities();
|
||||
}
|
||||
|
||||
setFooterStatus(__("Updating dashboard...", "really-simple-ssl") );
|
||||
await getProgressData();
|
||||
setFooterStatus( '' );
|
||||
setProcessing(false);
|
||||
}
|
||||
|
||||
if ( currentStep.id === 'email' ) {
|
||||
await saveEmail();
|
||||
setCurrentStepIndex(currentStepIndex+1);
|
||||
updateField('send_notifications_email', true );
|
||||
updateField('notifications_email_address', email );
|
||||
updateFieldsData(selectedSubMenuItem);
|
||||
}
|
||||
|
||||
if ( currentStep.id === 'plugins' ) {
|
||||
setCurrentStepIndex(currentStepIndex+1)
|
||||
for (const item of currentStep.items) {
|
||||
if (item.action !== 'none' && item.action !== null ) {
|
||||
// Add the promise returned by pluginInstaller to the array
|
||||
await pluginInstaller(item.id, item.action, item.title );
|
||||
}
|
||||
}
|
||||
setFooterStatus('')
|
||||
}
|
||||
|
||||
if (currentStep.id === 'pro') {
|
||||
if (rsssl_settings.pro_plugin_active) {
|
||||
setProcessing(true);
|
||||
//loop through all items of currentStep.items
|
||||
for (const item of currentStep.items) {
|
||||
if (item.activated) {
|
||||
if (item.id === 'advanced_headers') {
|
||||
for (const option of item.options) {
|
||||
if (typeof option === 'string') {
|
||||
// Single option
|
||||
updateField(option, true);
|
||||
setChangedField(option, true);
|
||||
} else if (Array.isArray(option)) {
|
||||
// [option -> value] pair
|
||||
const [fieldId, value] = option;
|
||||
updateField(fieldId, value);
|
||||
setChangedField(fieldId, value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const fieldId of Object.values(item.options)) {
|
||||
const value = item.value || item.activated;
|
||||
updateField(fieldId, value);
|
||||
setChangedField(fieldId, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setFooterStatus(__("Activating options...", "really-simple-ssl"));
|
||||
await saveFields(true, false);
|
||||
|
||||
setFooterStatus(__("Updating dashboard...", "really-simple-ssl"));
|
||||
await getProgressData();
|
||||
setFooterStatus('');
|
||||
setProcessing(false);
|
||||
}
|
||||
goToDashboard();
|
||||
}
|
||||
|
||||
if ( currentStep.id === 'activate_license' ) {
|
||||
if ( licenseStatus !== 'valid' ) {
|
||||
await toggleActivation(getFieldValue('license'));
|
||||
//if the license is valid, allow the user to go to the next step
|
||||
if ( licenseStatus === 'valid' ) {
|
||||
setCurrentStepIndex( currentStepIndex + 1 );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const handleActivateSSL = async () => {
|
||||
await activateSSL();
|
||||
await getProgressData();
|
||||
await fetchFieldsData();
|
||||
}
|
||||
|
||||
const goToLetsEncrypt = () => {
|
||||
if (isModal) dismissModal(true);
|
||||
window.location.href=rsssl_settings.letsencrypt_url;
|
||||
}
|
||||
|
||||
let ActivateSSLText = networkwide ? __("Activate SSL networkwide", "really-simple-ssl") : __("Activate SSL", "really-simple-ssl");
|
||||
if (currentStep.id === 'activate_ssl') {
|
||||
return (
|
||||
<>
|
||||
{isModal && !certificateValid && (
|
||||
<Button onClick={() => { goToLetsEncrypt() }}>
|
||||
{__("Install SSL", "really-simple-ssl")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
disabled={processing || (!certificateValid && !overrideSSL)}
|
||||
isPrimary
|
||||
onClick={() => { handleActivateSSL() }}
|
||||
>
|
||||
{ActivateSSLText}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentStep.id === 'activate_license') {
|
||||
return (
|
||||
<>
|
||||
<Button isPrimary onClick={() => saveAndContinue()}>
|
||||
{currentStep.button || __('Activate', 'really-simple-ssl')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentStepIndex>0 && currentStepIndex<steps.length-1 ) {
|
||||
return (
|
||||
<>
|
||||
{currentStep.id !== 'activate_license' && <Button onClick={() => {setCurrentStepIndex(currentStepIndex+1)}}>{__('Skip', 'really-simple-ssl')}</Button> }
|
||||
<Button isPrimary onClick={() => saveAndContinue() }>
|
||||
{currentStep.button}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
//for last step only
|
||||
if ( steps.length-1 === currentStepIndex ) {
|
||||
let upgradeText = rsssl_settings.is_bf ? __("Get 40% off", "really-simple-ssl") : __("Get Pro", "really-simple-ssl");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
isPrimary
|
||||
onClick={() => saveAndContinue()}
|
||||
disabled={ rsssl_settings.pro_plugin_active && licenseStatus !== 'valid' }
|
||||
>
|
||||
{__('Finish', 'really-simple-ssl')}
|
||||
</Button>
|
||||
{ !rsssl_settings.pro_plugin_active &&
|
||||
<Button
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
isPrimary
|
||||
href={rsssl_settings.upgrade_link}
|
||||
>
|
||||
{upgradeText}
|
||||
</Button>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OnboardingControls;
|
||||
@@ -0,0 +1,281 @@
|
||||
import {create} from 'zustand';
|
||||
import {produce} from 'immer';
|
||||
|
||||
import * as rsssl_api from "../utils/api";
|
||||
import {__} from "@wordpress/i18n";
|
||||
const useOnboardingData = create(( set, get ) => ({
|
||||
steps: [],
|
||||
currentStepIndex: 0,
|
||||
currentStep: {},
|
||||
error: false,
|
||||
networkProgress: 0,
|
||||
networkActivationStatus: '',
|
||||
certificateValid: '',
|
||||
networkwide: false,
|
||||
sslEnabled: false,
|
||||
overrideSSL: false,
|
||||
showOnboardingModal: false,
|
||||
modalStatusLoaded: false,
|
||||
dataLoaded: false,
|
||||
processing: false,
|
||||
email: '',
|
||||
includeTips:false,
|
||||
sendTestEmail:true,
|
||||
overrideSSLDetection:false,
|
||||
footerStatus: '',
|
||||
setFooterStatus: (footerStatus) => {
|
||||
set({footerStatus:footerStatus})
|
||||
},
|
||||
setIncludeTips: (includeTips) => {
|
||||
set(state => ({ includeTips }))
|
||||
},
|
||||
setSendTestEmail: (sendTestEmail) => {
|
||||
set(state => ({ sendTestEmail }))
|
||||
},
|
||||
setEmail: (email) => {
|
||||
set(state => ({ email }))
|
||||
},
|
||||
setShowOnboardingModal: (showOnboardingModal) => {
|
||||
set(state => ({ showOnboardingModal }))
|
||||
},
|
||||
setProcessing: (processing) => {
|
||||
set(state => ({ processing }))
|
||||
},
|
||||
setCurrentStepIndex: (currentStepIndex) => {
|
||||
const currentStep = get().steps[currentStepIndex];
|
||||
set(state => ({ currentStepIndex, currentStep }))
|
||||
},
|
||||
dismissModal: async (dismiss) => {
|
||||
let data={};
|
||||
data.dismiss = dismiss;
|
||||
//dismiss is opposite of showOnboardingModal, so we check the inverse.
|
||||
set(() => ({showOnboardingModal: !dismiss}));
|
||||
await rsssl_api.doAction('dismiss_modal', data);
|
||||
},
|
||||
setOverrideSSL: async (override) => {
|
||||
set({overrideSSL: override});
|
||||
let data = {
|
||||
overrideSSL: override,
|
||||
};
|
||||
await rsssl_api.doAction('override_ssl_detection',data );
|
||||
},
|
||||
activateSSL: () => {
|
||||
set((state) => ({processing:true}));
|
||||
rsssl_api.runTest('activate_ssl' ).then( async ( response ) => {
|
||||
set((state) => ({processing:false}));
|
||||
get().setCurrentStepIndex( get().currentStepIndex+1 );
|
||||
//change url to https, after final check
|
||||
if ( response.success ) {
|
||||
if ( response.site_url_changed ) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
if ( get().networkwide ) {
|
||||
set(state => ({ networkActivationStatus:'main_site_activated' }))
|
||||
}
|
||||
}
|
||||
|
||||
set({ sslEnabled: true})
|
||||
}
|
||||
});
|
||||
},
|
||||
saveEmail:() => {
|
||||
get().setFooterStatus( __("Updating email preferences..", "really-simple-ssl") );
|
||||
let data={};
|
||||
data.email = get().email;
|
||||
data.includeTips = get().includeTips;
|
||||
data.sendTestEmail = get().sendTestEmail;
|
||||
set((state) => ({processing:true}));
|
||||
rsssl_api.doAction('update_email', data).then(( response ) => {
|
||||
set((state) => ({processing:false}));
|
||||
get().setFooterStatus('' );
|
||||
});
|
||||
},
|
||||
updateItemStatus: (stepId, id, action, status, activated) => {
|
||||
const index = get().steps.findIndex(item => { return item.id===stepId; });
|
||||
const itemIndex = get().steps[index].items.findIndex(item => {return item.id===id;});
|
||||
set(
|
||||
produce((state) => {
|
||||
if (typeof action !== 'undefined') state.steps[index].items[itemIndex].action = action;
|
||||
if (typeof status !== 'undefined') state.steps[index].items[itemIndex].status = status;
|
||||
if (typeof activated !== 'undefined') state.steps[index].items[itemIndex].activated = activated;
|
||||
})
|
||||
)
|
||||
let currentStep = get().steps[get().currentStepIndex];
|
||||
set(
|
||||
produce((state) => {
|
||||
state.currentStep = currentStep;
|
||||
}
|
||||
))
|
||||
},
|
||||
fetchOnboardingModalStatus: async () => {
|
||||
rsssl_api.doAction('get_modal_status').then((response) => {
|
||||
set({
|
||||
showOnboardingModal: !response.dismissed,
|
||||
modalStatusLoaded: true,
|
||||
})
|
||||
});
|
||||
},
|
||||
setShowOnBoardingModal: (showOnboardingModal) => set(state => ({ showOnboardingModal })),
|
||||
pluginInstaller: async (id, action, title) => {
|
||||
if ( !action ) {
|
||||
return;
|
||||
}
|
||||
|
||||
set(() => ({processing:true}));
|
||||
get().updateItemStatus('plugins', id, action, 'processing');
|
||||
get().setFooterStatus(__("Installing %d...", "really-simple-ssl").replace("%d", title));
|
||||
|
||||
let nextAction = await processAction(action, id);
|
||||
get().updateItemStatus('plugins', id, nextAction);
|
||||
|
||||
if ( nextAction!=='none' && nextAction!=='completed') {
|
||||
get().setFooterStatus(__("Activating %d...", "really-simple-ssl").replace("%d", title));
|
||||
nextAction = await processAction(nextAction, id);
|
||||
get().updateItemStatus('plugins', id, nextAction);
|
||||
} else {
|
||||
get().setFooterStatus('');
|
||||
}
|
||||
set((state) => ({processing:false}));
|
||||
},
|
||||
getSteps: async (forceRefresh) => {
|
||||
const {steps, networkActivationStatus, certificateValid, networkProgress, networkwide,
|
||||
overrideSSL, error, sslEnabled, upgradedFromFree} = await retrieveSteps(forceRefresh);
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const isVerified = urlParams.get('verified_email') === '1';
|
||||
|
||||
let initialStepIndex = 0;
|
||||
let verified = isVerified;
|
||||
|
||||
// If verified, go to features step
|
||||
if (isVerified) {
|
||||
const featuresIndex = steps.findIndex(step => step.id === 'features');
|
||||
if (featuresIndex !== -1) {
|
||||
initialStepIndex = featuresIndex;
|
||||
// Remove the parameter from URL
|
||||
urlParams.delete('verified_email');
|
||||
const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
} else if (!upgradedFromFree && (sslEnabled || (networkwide && networkActivationStatus === 'completed'))) {
|
||||
initialStepIndex = 1;
|
||||
}
|
||||
|
||||
// Set all state at once to prevent multiple rerenders
|
||||
set({
|
||||
steps,
|
||||
currentStepIndex: initialStepIndex,
|
||||
currentStep: steps[initialStepIndex],
|
||||
networkActivationStatus,
|
||||
certificateValid,
|
||||
networkProgress,
|
||||
networkwide,
|
||||
overrideSSL,
|
||||
sslEnabled,
|
||||
dataLoaded: true,
|
||||
error,
|
||||
emailVerified: verified
|
||||
});
|
||||
|
||||
if (networkActivationStatus === 'completed') {
|
||||
set({networkProgress: 100});
|
||||
}
|
||||
},
|
||||
refreshSSLStatus: (e) => {
|
||||
e.preventDefault();
|
||||
set( {processing: true} );
|
||||
set(
|
||||
produce((state) => {
|
||||
const stepIndex = state.steps.findIndex(step => {
|
||||
return step.id==='activate_ssl';
|
||||
});
|
||||
const step = state.steps[stepIndex];
|
||||
step.items.forEach(function(item, j){
|
||||
if (item.status==='error') {
|
||||
step.items[j].status = 'processing';
|
||||
step.items[j].title = __("Re-checking SSL certificate, please wait...","really-simple-ssl");
|
||||
}
|
||||
});
|
||||
state.steps[stepIndex] = step;
|
||||
})
|
||||
)
|
||||
|
||||
setTimeout(async function () {
|
||||
const {
|
||||
steps,
|
||||
certificateValid,
|
||||
error,
|
||||
} = await retrieveSteps(true);
|
||||
set({
|
||||
steps: steps,
|
||||
certificateValid: certificateValid,
|
||||
processing: false,
|
||||
error: error,
|
||||
});
|
||||
}, 1000) //add a delay, otherwise it's so fast the user may not trust it.
|
||||
},
|
||||
activateSSLNetworkWide: () => {
|
||||
let progress = get().networkProgress;
|
||||
if (typeof progress !== 'undefined') {
|
||||
get().setFooterStatus(__("%d% of subsites activated.").replace('%d', progress));
|
||||
}
|
||||
if (get().networkProgress>=100) {
|
||||
set({
|
||||
sslEnabled: true,
|
||||
networkActivationStatus:'completed'
|
||||
});
|
||||
return;
|
||||
}
|
||||
set( () => ({processing: true}));
|
||||
rsssl_api.runTest('activate_ssl_networkwide' ).then( ( response ) => {
|
||||
if (response.success) {
|
||||
set({
|
||||
networkProgress: response.progress,
|
||||
processing:false,
|
||||
});
|
||||
get().setFooterStatus(__("%d% of subsites activated.").replace('%d', response.progress));
|
||||
|
||||
if (response.progress>=100) {
|
||||
get().setFooterStatus('');
|
||||
set({
|
||||
sslEnabled: true,
|
||||
networkActivationStatus:'completed'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
const retrieveSteps = (forceRefresh) => {
|
||||
let data={};
|
||||
data.forceRefresh = forceRefresh;
|
||||
return rsssl_api.doAction('onboarding_data', data).then( ( response ) => {
|
||||
let steps = response.steps;
|
||||
let sslEnabled= response.ssl_enabled;
|
||||
let networkActivationStatus= response.network_activation_status;
|
||||
let certificateValid = response.certificate_valid;
|
||||
let networkProgress = response.network_progress;
|
||||
let networkwide = response.networkwide;
|
||||
let overrideSSL = response.ssl_detection_overridden;
|
||||
let error = response.error;
|
||||
let upgradedFromFree = response.rsssl_upgraded_from_free;
|
||||
return {steps, networkActivationStatus, certificateValid, networkProgress, networkwide, overrideSSL, error, sslEnabled, upgradedFromFree};
|
||||
});
|
||||
}
|
||||
|
||||
const processAction = async (action, id) => {
|
||||
let data={};
|
||||
data.id = id;
|
||||
return await rsssl_api.doAction(action, data).then( async ( response ) => {
|
||||
if ( response.success ){
|
||||
return response.next_action;
|
||||
} else {
|
||||
return 'failed';
|
||||
}
|
||||
}).catch(error => {
|
||||
return 'failed';
|
||||
});
|
||||
}
|
||||
|
||||
export default useOnboardingData;
|
||||
@@ -0,0 +1,79 @@
|
||||
import {useEffect} from "@wordpress/element";
|
||||
import Onboarding from "./Onboarding";
|
||||
import Placeholder from '../Placeholder/Placeholder';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import Icon from "../utils/Icon";
|
||||
import useOnboardingData from "./OnboardingData";
|
||||
import useFields from "../Settings/FieldsData";
|
||||
import RssslModal from "../../../modal/src/components/Modal/RssslModal";
|
||||
import OnboardingControls from "./OnboardingControls";
|
||||
|
||||
const OnboardingModal = () => {
|
||||
const {footerStatus, showOnboardingModal, fetchOnboardingModalStatus, modalStatusLoaded, currentStep, dismissModal} = useOnboardingData();
|
||||
const {fieldsLoaded} = useFields();
|
||||
|
||||
useEffect(() => {
|
||||
if ( !modalStatusLoaded ) {
|
||||
fetchOnboardingModalStatus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(()=> {
|
||||
if ( showOnboardingModal ) {
|
||||
dismissModal(false);
|
||||
}
|
||||
}, [showOnboardingModal]);
|
||||
|
||||
const modalContent = () => {
|
||||
return (
|
||||
<>
|
||||
{ !fieldsLoaded &&
|
||||
<>
|
||||
<ul>
|
||||
<li><Icon name = "loading" />{__("Please wait while we detect your setup", "really-simple-ssl")}</li>
|
||||
</ul>
|
||||
<Placeholder lines="3"></Placeholder>
|
||||
</>
|
||||
}
|
||||
{ fieldsLoaded && <Onboarding isModal={true} /> }
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const setOpen = (open) => {
|
||||
if ( !open ) {
|
||||
dismissModal(true);
|
||||
}
|
||||
}
|
||||
|
||||
const handleFooterStatus = () => {
|
||||
if ( footerStatus.length === 0 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Icon name = "loading" color = 'grey' />
|
||||
{footerStatus}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RssslModal
|
||||
className={"rsssl-onboarding-modal"}
|
||||
title={currentStep.title}
|
||||
subTitle={currentStep.subtitle}
|
||||
currentStep = {currentStep}
|
||||
content={modalContent()}
|
||||
isOpen={showOnboardingModal}
|
||||
setOpen={setOpen}
|
||||
buttons = <OnboardingControls isModal={true} />
|
||||
footer = {handleFooterStatus() }
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default OnboardingModal;
|
||||
@@ -0,0 +1,6 @@
|
||||
.rsssl-modal-premium-container {
|
||||
background-color: var(--rsp-dark-blue);
|
||||
color:#fff;
|
||||
padding:0 5px;
|
||||
margin-right:22px;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { memo, useEffect } from "@wordpress/element";
|
||||
import { __ } from "@wordpress/i18n";
|
||||
import useOnboardingData from "../OnboardingData";
|
||||
import useFields from "../../Settings/FieldsData";
|
||||
import Host from "../../Settings/Host/Host";
|
||||
import ListItem from "../Items/ListItem";
|
||||
|
||||
const StepConfig = ({ isModal }) => {
|
||||
const { fetchFieldsData, getField, fieldsLoaded, updateField, setChangedField, saveFields } = useFields();
|
||||
const { currentStep } = useOnboardingData();
|
||||
|
||||
useEffect(() => {
|
||||
if (!fieldsLoaded) {
|
||||
fetchFieldsData();
|
||||
}
|
||||
}, []);
|
||||
|
||||
let otherHostsField = fieldsLoaded && getField('other_host_type');
|
||||
let items = currentStep.items ? currentStep.items : [];
|
||||
|
||||
if (rsssl_settings.cloudflare && !items.some(item => item.id === 'cf')) {
|
||||
let cfItem = {
|
||||
status: 'success',
|
||||
title: "CloudFlare",
|
||||
id: 'cf'
|
||||
};
|
||||
items.unshift(cfItem);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isModal && <Host field={otherHostsField} showDisabledWhenSaving={false} />}
|
||||
<ul>
|
||||
{items && items.map((item, index) => <ListItem key={'step-config-' + index} item={item} />)}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(StepConfig);
|
||||
@@ -0,0 +1,65 @@
|
||||
import {memo, useEffect} from "@wordpress/element";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import useOnboardingData from "../OnboardingData";
|
||||
import useFields from "../../Settings/FieldsData";
|
||||
|
||||
const StepEmail = () => {
|
||||
const { fetchFieldsData, getFieldValue, fieldsLoaded} = useFields();
|
||||
const {
|
||||
email,
|
||||
setEmail,
|
||||
includeTips,
|
||||
setIncludeTips,
|
||||
} = useOnboardingData();
|
||||
|
||||
// Initialize state if needed
|
||||
useEffect(() => {
|
||||
if (!fieldsLoaded) {
|
||||
fetchFieldsData();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Set initial email if available
|
||||
useEffect(() => {
|
||||
const savedEmail = getFieldValue('notifications_email_address');
|
||||
if (savedEmail && !email) {
|
||||
setEmail(savedEmail);
|
||||
}
|
||||
}, [fieldsLoaded, getFieldValue, email, setEmail]);
|
||||
|
||||
if (!fieldsLoaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rsssl-step-email">
|
||||
<div className="rsssl-email-input">
|
||||
<input
|
||||
type="email"
|
||||
value={email || ''}
|
||||
placeholder={__("Your email address", "really-simple-ssl")}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="rsssl-email-options">
|
||||
<label className="rsssl-tips-checkbox">
|
||||
<input
|
||||
onChange={(e) => setIncludeTips(e.target.checked)}
|
||||
type="checkbox"
|
||||
checked={!!includeTips}
|
||||
/>
|
||||
<span>{__("Include 6 Tips & Tricks to get started with Really Simple Security.", "really-simple-ssl")}</span>
|
||||
<a
|
||||
href="https://really-simple-ssl.com/legal/privacy-statement/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{__("Privacy Statement", "really-simple-ssl")}
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(StepEmail)
|
||||
@@ -0,0 +1,35 @@
|
||||
import {memo} from "@wordpress/element";
|
||||
import useOnboardingData from "../OnboardingData";
|
||||
import CheckboxItem from "../Items/CheckboxItem";
|
||||
import PremiumItem from "../Items/PremiumItem";
|
||||
|
||||
const StepFeatures = () => {
|
||||
const {
|
||||
currentStep
|
||||
} = useOnboardingData();
|
||||
|
||||
let items = currentStep.items ? currentStep.items : [];
|
||||
let freeItems = items.filter( (item) => !item.premium );
|
||||
let premiumItems = items.filter( (item) => item.premium );
|
||||
return (
|
||||
<>
|
||||
<ul>
|
||||
{freeItems && (
|
||||
<div className="rsssl-checkbox-items">
|
||||
{freeItems.map((item, index) => (
|
||||
<CheckboxItem key={'step-features' + index} item={item}/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{premiumItems && (
|
||||
<div className="rsssl-premium-items">
|
||||
{premiumItems.map((item, index) => (
|
||||
<PremiumItem key={'step-features' + index} item={item}/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default memo(StepFeatures)
|
||||
@@ -0,0 +1,33 @@
|
||||
import {memo, useEffect, useRef} from "@wordpress/element";
|
||||
import useOnboardingData from "../OnboardingData";
|
||||
import License from "../../Settings/License/License";
|
||||
import useFields from "../../Settings/FieldsData";
|
||||
import useLicense from "../../Settings/License/LicenseData";
|
||||
|
||||
const StepLicense = () => {
|
||||
const {
|
||||
currentStepIndex,
|
||||
setCurrentStepIndex,
|
||||
} = useOnboardingData();
|
||||
const { getField } = useFields();
|
||||
const {licenseStatus} = useLicense();
|
||||
const pro_plugin_active = rsssl_settings.pro_plugin_active;
|
||||
|
||||
//skip step if either already active, or if not pro
|
||||
useEffect( () => {
|
||||
if ( ! pro_plugin_active || licenseStatus === 'valid' ) {
|
||||
setCurrentStepIndex(currentStepIndex + 1);
|
||||
}
|
||||
}, [licenseStatus, pro_plugin_active] );
|
||||
|
||||
return (
|
||||
<div className={"rsssl-license"}>
|
||||
<License
|
||||
field={getField('license')}
|
||||
isOnboarding={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(StepLicense);
|
||||
@@ -0,0 +1,30 @@
|
||||
import {memo, useEffect} from "@wordpress/element";
|
||||
import useOnboardingData from "../OnboardingData";
|
||||
import CheckboxItem from "../Items/CheckboxItem";
|
||||
|
||||
const StepPlugins = () => {
|
||||
const {
|
||||
currentStep,
|
||||
currentStepIndex,
|
||||
setCurrentStepIndex,
|
||||
} = useOnboardingData();
|
||||
|
||||
useEffect(()=> {
|
||||
//if all plugins are already activated, we skip the plugins step
|
||||
let plugins = currentStep.items;
|
||||
if ( plugins.filter(item => item.action !== 'none').length === 0) {
|
||||
setCurrentStepIndex(currentStepIndex+1);
|
||||
}
|
||||
}, [] );
|
||||
|
||||
let plugins = currentStep.items;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ul>
|
||||
{ plugins && plugins.map( (item, index) => <CheckboxItem key={'step-plugins'+index} item={item} disabled={item.action==='none'} />) }
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default memo(StepPlugins);
|
||||
@@ -0,0 +1,33 @@
|
||||
import {memo} from "@wordpress/element";
|
||||
import useOnboardingData from "../OnboardingData";
|
||||
import CheckboxItem from "../Items/CheckboxItem";
|
||||
import PremiumItem from "../Items/PremiumItem";
|
||||
|
||||
const StepPro = () => {
|
||||
const {
|
||||
currentStep,
|
||||
} = useOnboardingData();
|
||||
|
||||
let premiumItems = currentStep.items;
|
||||
return (
|
||||
<>
|
||||
<ul>
|
||||
{!rsssl_settings.pro_plugin_active && premiumItems && (
|
||||
<div className="rsssl-premium-items">
|
||||
{premiumItems.map((item, index) => (
|
||||
<PremiumItem key={'step-pro' + index} item={item}/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{rsssl_settings.pro_plugin_active && premiumItems && (
|
||||
<div className="rsssl-checkbox-items">
|
||||
{premiumItems.map((item, index) => (
|
||||
<CheckboxItem key={'step-pro' + index} item={item}/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default memo(StepPro);
|
||||
@@ -0,0 +1,73 @@
|
||||
.rsssl-modal-body, .rsssl-le-activate_ssl {
|
||||
.rsssl-modal-checkbox-container {
|
||||
display: block;
|
||||
position: relative;
|
||||
margin-bottom: 5px;
|
||||
padding-left: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
overflow:unset !important;
|
||||
|
||||
&:hover input ~ .rsssl-checkmark {
|
||||
background-color: var(--rsp-grey-400);
|
||||
border: 1px solid var(--rsp-grey-400);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
input:checked ~ .rsssl-checkmark {
|
||||
background-color: var(--rsp-dark-blue);
|
||||
border: 1px solid var(--rsp-dark-blue);
|
||||
|
||||
}
|
||||
|
||||
input:disabled ~ .rsssl-checkmark {
|
||||
background-color: var(--rsp-grey-400);
|
||||
border: 1px solid var(--rsp-grey-400);
|
||||
|
||||
}
|
||||
|
||||
input:checked ~ .rsssl-checkmark:after {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.rsssl-checkmark::after {
|
||||
left: 6px;
|
||||
top: 3px;
|
||||
width: 5px;
|
||||
height: 7px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
-webkit-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
}
|
||||
|
||||
input {
|
||||
height: 0;
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rsssl-checkmark {
|
||||
background-color: var(--rsp-grey-300);
|
||||
border: 1px solid var(--rsp-grey-400);
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: var(--rsp-fs-600);
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.rsssl-checkmark::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
.rsssl-modal.rsssl-onboarding-modal .rsssl-modal-body, .rsssl-letsencrypt .rsssl-le-activate_ssl {
|
||||
|
||||
.rsssl-override-detection-toggle {
|
||||
margin-bottom: -15px;
|
||||
}
|
||||
input[type=email] {
|
||||
width: 100%;
|
||||
margin-bottom: var(--rsp-spacing-m);
|
||||
border: 2px solid;
|
||||
height: 50px;
|
||||
border-color: var(--rsp-grey-300);
|
||||
}
|
||||
//hide select label
|
||||
.rsssl-select {
|
||||
label {
|
||||
display: none;
|
||||
}
|
||||
.components-select-control__input{
|
||||
height:45px;
|
||||
padding: 8px 20px;
|
||||
color: var(--rsp-grey-500);
|
||||
}
|
||||
}
|
||||
|
||||
.rsssl-activate_ssl, .rsssl-plugins {
|
||||
ul {
|
||||
column-count: 1;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--rsp-spacing-xxs);
|
||||
&.rsssl-is-plugin{
|
||||
background-color: var(--rsp-grey-100);
|
||||
border: none;
|
||||
margin: 10px 0;
|
||||
padding:5px 0;
|
||||
position:relative;
|
||||
}
|
||||
.rsssl-icon{
|
||||
margin-right:7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import {useEffect, useState} from "@wordpress/element";
|
||||
import Header from "./Header";
|
||||
import PagePlaceholder from './Placeholder/PagePlaceholder';
|
||||
import getAnchor from "./utils/getAnchor";
|
||||
import useFields from "./Settings/FieldsData";
|
||||
import useMenu from "./Menu/MenuData";
|
||||
import useOnboardingData from "./Onboarding/OnboardingData";
|
||||
import useModal from "./Modal/ModalData";
|
||||
import {setLocaleData} from "@wordpress/i18n";
|
||||
import ErrorBoundary from "./utils/ErrorBoundary";
|
||||
const Page = () => {
|
||||
const {error, fields, changedFields, fetchFieldsData, updateFieldsData, fieldsLoaded} = useFields();
|
||||
const {showOnboardingModal, fetchOnboardingModalStatus, modalStatusLoaded,} = useOnboardingData();
|
||||
const {selectedMainMenuItem, fetchMenuData } = useMenu();
|
||||
const {showModal} = useModal();
|
||||
|
||||
const [Settings, setSettings] = useState(null);
|
||||
const [DashboardPage, setDashboardPage] = useState(null);
|
||||
const [Notices, setNotices] = useState(null);
|
||||
const [Menu, setMenu] = useState(null);
|
||||
const [ToastContainer, setToastContainer] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if ( !modalStatusLoaded ) {
|
||||
fetchOnboardingModalStatus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
//load the chunk translations passed to us from the rsssl_settings object
|
||||
//only works in build mode, not in dev mode.
|
||||
useEffect(() => {
|
||||
rsssl_settings.json_translations.forEach( (translationsString) => {
|
||||
let translations = JSON.parse(translationsString);
|
||||
let localeData = translations.locale_data[ 'really-simple-ssl' ] || translations.locale_data.messages;
|
||||
localeData[""].domain = 'really-simple-ssl';
|
||||
setLocaleData( localeData, 'really-simple-ssl' );
|
||||
});
|
||||
},[]);
|
||||
|
||||
useEffect( () => {
|
||||
if (selectedMainMenuItem !== 'dashboard' ){
|
||||
if (!Settings) {
|
||||
import ("./Settings/Settings").then(({default: Settings}) => {
|
||||
setSettings(() => Settings);
|
||||
});
|
||||
}
|
||||
if (!Notices) {
|
||||
import("./Settings/Notices").then(({default: Notices}) => {
|
||||
setNotices(() => Notices);
|
||||
});
|
||||
}
|
||||
if (!Menu) {
|
||||
import ("./Menu/Menu").then(({default: Menu}) => {
|
||||
setMenu(() => Menu);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (selectedMainMenuItem === 'dashboard' && !DashboardPage ){
|
||||
import ( "./Dashboard/DashboardPage").then(async ({default: DashboardPage}) => {
|
||||
setDashboardPage(() => DashboardPage);
|
||||
});
|
||||
}
|
||||
|
||||
}, [selectedMainMenuItem]);
|
||||
|
||||
const [OnboardingModal, setOnboardingModal] = useState(null);
|
||||
useEffect( () => {
|
||||
if ( showOnboardingModal && !OnboardingModal ){
|
||||
import ("./Onboarding/OnboardingModal").then(({ default: OnboardingModal }) => {
|
||||
setOnboardingModal(() => OnboardingModal);
|
||||
});
|
||||
}
|
||||
|
||||
}, [showOnboardingModal]);
|
||||
|
||||
const [Modal, setModal] = useState(null);
|
||||
useEffect( () => {
|
||||
if ( showModal && !Modal ){
|
||||
import ( "./Modal/Modal").then(({ default: Modal }) => {
|
||||
setModal(() => Modal);
|
||||
});
|
||||
}
|
||||
|
||||
}, [showModal]);
|
||||
|
||||
// async load react-toastify
|
||||
useEffect(() => {
|
||||
import('react-toastify').then((module) => {
|
||||
const ToastContainer = module.ToastContainer;
|
||||
setToastContainer(() => ToastContainer);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect( () => {
|
||||
if ( fieldsLoaded ) {
|
||||
fetchMenuData(fields);
|
||||
window.addEventListener('hashchange', (e) => {
|
||||
fetchMenuData(fields);
|
||||
});
|
||||
}
|
||||
}, [fields] );
|
||||
|
||||
useEffect( () => {
|
||||
let subMenuItem = getAnchor('menu');
|
||||
updateFieldsData(subMenuItem);
|
||||
}, [changedFields] );
|
||||
|
||||
useEffect( () => {
|
||||
let subMenuItem = getAnchor('menu');
|
||||
fetchFieldsData(subMenuItem);
|
||||
}, [] );
|
||||
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
<PagePlaceholder error={error}></PagePlaceholder>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="rsssl-wrapper">
|
||||
{OnboardingModal && <ErrorBoundary fallback={"Could not load onboarding modal"}><OnboardingModal /></ErrorBoundary>}
|
||||
|
||||
{Modal && <ErrorBoundary fallback={"Could not load modal"}><Modal/></ErrorBoundary>}
|
||||
{
|
||||
<>
|
||||
<Header />
|
||||
<div className={"rsssl-content-area rsssl-grid rsssl-" + selectedMainMenuItem}>
|
||||
{ selectedMainMenuItem !== 'dashboard' && Settings && Menu && Notices &&
|
||||
<>
|
||||
<ErrorBoundary fallback={"Could not load menu"}><Menu /></ErrorBoundary>
|
||||
<ErrorBoundary fallback={"Could not load settings"}><Settings/></ErrorBoundary>
|
||||
<ErrorBoundary fallback={"Could not load notices"}><Notices className="rsssl-wizard-notices"/></ErrorBoundary>
|
||||
</>
|
||||
}
|
||||
{ selectedMainMenuItem === 'dashboard' && DashboardPage &&
|
||||
<ErrorBoundary fallback={"Could not load menu"}><DashboardPage /></ErrorBoundary>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
|
||||
}
|
||||
{ToastContainer && (
|
||||
<ToastContainer
|
||||
position="bottom-right"
|
||||
autoClose={2000}
|
||||
limit={3}
|
||||
hideProgressBar
|
||||
newestOnTop
|
||||
closeOnClick
|
||||
pauseOnFocusLoss
|
||||
pauseOnHover
|
||||
theme="light"
|
||||
/> )}
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
export default Page
|
||||
@@ -0,0 +1,12 @@
|
||||
const DashboardPlaceholder = (props) => {
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-grid-item rsssl-column-2 rsssl-dashboard-placeholder"></div>
|
||||
<div className="rsssl-grid-item rsssl-row-2 rsssl-dashboard-placeholder"></div>
|
||||
<div className="rsssl-grid-item rsssl-row-2 rsssl-dashboard-placeholder"></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardPlaceholder;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
const DatatablePlaceholder = (props) => {
|
||||
let lines = props.lines;
|
||||
if ( !lines ) lines = 3;
|
||||
return (
|
||||
<div className="rsssl-datatable-placeholder">
|
||||
{Array.from({length: lines}).map((item, i) => (<div key={'datatable-placeholder-'+i} ></div>))}
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default DatatablePlaceholder;
|
||||
@@ -0,0 +1,12 @@
|
||||
const MenuPlaceholder = () => {
|
||||
return (
|
||||
<div className="rsssl-wizard-menu rsssl-grid-item rsssl-menu-placeholder">
|
||||
<div className="rsssl-grid-item-header">
|
||||
<h1 className="rsssl-h4"></h1>
|
||||
</div>
|
||||
<div className="rsssl-grid-item-content"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MenuPlaceholder;
|
||||
@@ -0,0 +1,25 @@
|
||||
import Error from '../utils/Error';
|
||||
const PagePlaceholder = (props) => {
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-header-container">
|
||||
<div className="rsssl-header">
|
||||
<img className="rsssl-logo"
|
||||
src={rsssl_settings.plugin_url + 'assets/img/really-simple-security-logo.svg'}
|
||||
alt="Really Simple Security logo"/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rsssl-content-area rsssl-grid rsssl-dashboard rsssl-page-placeholder">
|
||||
<div className="rsssl-grid-item rsssl-column-2 rsssl-row-2 ">
|
||||
{props.error && <Error error={props.error} /> }
|
||||
</div>
|
||||
<div className="rsssl-grid-item rsssl-row-2"></div>
|
||||
<div className="rsssl-grid-item rsssl-row-2"></div>
|
||||
<div className="rsssl-grid-item rsssl-column-2"></div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PagePlaceholder;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import Error from "../utils/Error";
|
||||
|
||||
const Placeholder = (props) => {
|
||||
|
||||
let lines = props.lines;
|
||||
if ( !lines ) lines = 4;
|
||||
if (props.error) {
|
||||
lines = 0;
|
||||
}
|
||||
return (
|
||||
<div className="rsssl-placeholder">
|
||||
{props.error && <Error error={props.error} /> }
|
||||
{Array.from({length: lines}).map((item, i) => (<div className="rsssl-placeholder-line" key={"placeholder-"+i} ></div>))}
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default Placeholder;
|
||||
@@ -0,0 +1,19 @@
|
||||
import Placeholder from "./Placeholder";
|
||||
|
||||
/**
|
||||
* Menu block, rendering the entire menu
|
||||
*/
|
||||
const SettingsPlaceholder = () => {
|
||||
return(
|
||||
<div className="rsssl-wizard-settings rsssl-column-2 rsssl-settings-placeholder">
|
||||
<div className="rsssl-grid-item">
|
||||
<div className="rsssl-grid-item-content">
|
||||
<div className="rsssl-settings-block-intro"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rsssl-grid-item-footer"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsPlaceholder;
|
||||
@@ -0,0 +1,41 @@
|
||||
.rsssl-modal-body, .rsssl {
|
||||
input.MuiInput-underline:before {
|
||||
display:none;
|
||||
}
|
||||
.MuiAutocomplete-root {
|
||||
.MuiInputLabel-outlined[data-shrink=false] {
|
||||
transform: translate(14px, 16px) scale(1);
|
||||
}
|
||||
.MuiFormLabel-root {
|
||||
font-family:inherit;
|
||||
}
|
||||
.MuiOutlinedInput-root {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
input.MuiAutocomplete-input[type=text] {
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow:none;
|
||||
}
|
||||
border:0;
|
||||
padding-left:12px;
|
||||
}
|
||||
.MuiInputBase-root {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.MuiInput-root input.MuiInputBase-input {
|
||||
padding-left:10px;
|
||||
}
|
||||
.MuiPopper-root, .MuiPaper-root {
|
||||
max-height:150px;
|
||||
z-index: 999999;
|
||||
div {
|
||||
font-family: inherit !important;
|
||||
}
|
||||
ul {
|
||||
max-height:initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* The native selectControl doesn't allow disabling per option.
|
||||
*/
|
||||
|
||||
import DOMPurify from "dompurify";
|
||||
import {Autocomplete} from "@mui/material";
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import './AutoComplete.scss';
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
autoComplete: {
|
||||
fontSize: "12px"
|
||||
}
|
||||
}));
|
||||
|
||||
const AutoCompleteControl = ({field, disabled, value, options, label, onChange }) => {
|
||||
let selectDisabled = !Array.isArray(disabled) && disabled;
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Autocomplete
|
||||
classes={{
|
||||
input: classes.autoComplete,
|
||||
option: classes.autoComplete
|
||||
}}
|
||||
disabled={selectDisabled}
|
||||
disablePortal
|
||||
value={ value }
|
||||
id={field.id}
|
||||
options={options}
|
||||
isOptionEqualToValue={(option, value) => {
|
||||
const optionValue = typeof option.value === "string" ? option.value.toLowerCase() : option.value;
|
||||
const valueValue = typeof value.value === "string" ? value.value.toLowerCase() : value.value;
|
||||
return optionValue === valueValue;
|
||||
}}
|
||||
getOptionLabel={(option) => {
|
||||
if ( option && option.label ) {
|
||||
return option.label;
|
||||
}
|
||||
const selectedOption = options.find( item => item.value === option );
|
||||
if ( selectedOption ) {
|
||||
return selectedOption.label;
|
||||
}
|
||||
return option;
|
||||
} }
|
||||
onChange={(event, newValue) => {
|
||||
let value = newValue && newValue.value ? newValue.value : '';
|
||||
onChange(value);
|
||||
}}
|
||||
renderInput={(params) => <TextField {...params}
|
||||
label={label}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default AutoCompleteControl
|
||||
@@ -0,0 +1,45 @@
|
||||
import Hyperlink from "../utils/Hyperlink";
|
||||
import * as rsssl_api from "../utils/api";
|
||||
import useFields from "./FieldsData";
|
||||
import Icon from "../utils/Icon";
|
||||
import {useState} from "@wordpress/element";
|
||||
|
||||
/**
|
||||
* Render a help notice in the sidebar
|
||||
*/
|
||||
const Button = (props) => {
|
||||
const {addHelpNotice} = useFields();
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const {fields} = useFields();
|
||||
|
||||
const onClickHandler = async (action) => {
|
||||
let data = {};
|
||||
setProcessing(true);
|
||||
data.fields = fields;
|
||||
await rsssl_api.doAction(action, data).then((response) => {
|
||||
let label = response.success ? 'success' : 'warning';
|
||||
let title = response.title;
|
||||
let text = response.message;
|
||||
setProcessing(false);
|
||||
addHelpNotice(props.field.id, label, text, title, false);
|
||||
});
|
||||
}
|
||||
|
||||
let is_disabled = !!props.field.disabled;
|
||||
|
||||
return (
|
||||
<>
|
||||
{ props.field.url &&
|
||||
<Hyperlink className={"button button-default"} disabled={is_disabled} text={props.field.button_text} url={props.field.url}/>
|
||||
}
|
||||
{ props.field.action &&
|
||||
<button onClick={ () => onClickHandler( props.field.action ) } className="button button-default" disabled={is_disabled}>
|
||||
{props.field.button_text}
|
||||
{processing && <Icon name = "loading" color = 'grey' />}
|
||||
</button>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Button
|
||||
@@ -0,0 +1,89 @@
|
||||
import ReCaptcha from './ReCaptcha';
|
||||
import HCaptcha from './HCaptcha';
|
||||
import useFields from '../FieldsData';
|
||||
import useCaptchaData from "./CaptchaData";
|
||||
import {__} from '@wordpress/i18n';
|
||||
import {useEffect, useState} from "@wordpress/element";
|
||||
import ErrorBoundary from "../../utils/ErrorBoundary";
|
||||
import Button from "../Button";
|
||||
|
||||
const Captcha = ({props}) => {
|
||||
const {getFieldValue, updateField, saveFields, getField} = useFields();
|
||||
const enabled_captcha_provider = getFieldValue('enabled_captcha_provider');
|
||||
const siteKey = getFieldValue(`${enabled_captcha_provider}_site_key`);
|
||||
const secretKey = getFieldValue(`${enabled_captcha_provider}_secret_key` );
|
||||
const fully_enabled = getFieldValue('captcha_fully_enabled');
|
||||
const {verifyCaptcha, setReloadCaptcha, removeRecaptchaScript} = useCaptchaData();
|
||||
const [showCaptcha, setShowCaptcha] = useState(false);
|
||||
const [buttonEnabled, setButtonEnabled] = useState(false);
|
||||
|
||||
|
||||
|
||||
const handleCaptchaResponse = (response) => {
|
||||
verifyCaptcha(response).then((response) => {
|
||||
if (response && response.success) {
|
||||
updateField('captcha_fully_enabled', 1);
|
||||
saveFields(false, false, true);
|
||||
} else {
|
||||
updateField('captcha_fully_enabled', false);
|
||||
saveFields(false, false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//if we switch to another captcha provider, we need to reset the captcha
|
||||
useEffect(() => {
|
||||
saveFields(false, false);
|
||||
}, [enabled_captcha_provider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fully_enabled) {
|
||||
updateField('captcha_fully_enabled', 1);
|
||||
saveFields(false, false);
|
||||
}
|
||||
}, [fully_enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowCaptcha(false);
|
||||
//based on the provider the keys need certain length if hcapthca the length is 36 and recapthca 40
|
||||
switch (enabled_captcha_provider) {
|
||||
case 'recaptcha':
|
||||
if (siteKey.length === 40 && secretKey.length === 40) {
|
||||
setButtonEnabled(true);
|
||||
} else {
|
||||
setButtonEnabled(false);
|
||||
}
|
||||
break;
|
||||
case 'hcaptcha':
|
||||
if (siteKey.length === 36 && secretKey.length === 35) {
|
||||
setButtonEnabled(true);
|
||||
} else {
|
||||
setButtonEnabled(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}, [siteKey, secretKey, enabled_captcha_provider]);
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ErrorBoundary title={__('Reload Captcha' , 'really-simple-ssl')}>
|
||||
{enabled_captcha_provider === 'recaptcha' && !fully_enabled && showCaptcha && (
|
||||
<ReCaptcha handleCaptchaResponse={handleCaptchaResponse} />
|
||||
)}
|
||||
{enabled_captcha_provider === 'hcaptcha' && !fully_enabled && showCaptcha && (
|
||||
<HCaptcha sitekey={siteKey} handleCaptchaResponse={handleCaptchaResponse} captchaVerified={fully_enabled}/>
|
||||
)}
|
||||
{enabled_captcha_provider !== 'none' && !fully_enabled && (
|
||||
<button
|
||||
disabled={!buttonEnabled}
|
||||
className={`button button-primary ${!buttonEnabled ? 'rsssl-learning-mode-disabled' : ''}`}
|
||||
// style={{display: !showCaptcha? 'none': 'block'}}
|
||||
onClick={() => setShowCaptcha(true)}> {__('Validate CAPTCHA', 'really-simple-ssl')} </button>)
|
||||
}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Captcha;
|
||||
@@ -0,0 +1,40 @@
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
|
||||
const useCaptchaData = create(( set, get ) => ({
|
||||
reloadCaptcha: false,
|
||||
setReloadCaptcha: ( value ) => set({ reloadCaptcha: value }),
|
||||
verifyCaptcha: async ( responseToken ) => {
|
||||
try {
|
||||
const response = await rsssl_api.doAction('verify_captcha', { responseToken: responseToken });
|
||||
|
||||
// Handle the response
|
||||
if ( !response ) {
|
||||
console.error('No response received from the server.');
|
||||
return;
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
},
|
||||
removeRecaptchaScript: async(source = 'recaptcha') => {
|
||||
if (window.grecaptcha) {
|
||||
window.grecaptcha.reset();
|
||||
delete window.grecaptcha;
|
||||
}
|
||||
const scriptTags = document.querySelectorAll('script[src^="https://www.google.com/recaptcha/api.js"]');
|
||||
// For each found script tag
|
||||
scriptTags.forEach((scriptTag) => {
|
||||
scriptTag.remove(); // Remove it
|
||||
});
|
||||
const rescriptTags = document.querySelectorAll('script[src^="https://www.google.com/recaptcha/api.js"]');
|
||||
// now we check if reCaptcha was still rendered.
|
||||
const recaptchaContainer = document.getElementById('recaptchaContainer');
|
||||
if (recaptchaContainer) {
|
||||
recaptchaContainer.remove();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export default useCaptchaData;
|
||||
@@ -0,0 +1,39 @@
|
||||
import Icon from "../../utils/Icon";
|
||||
import useFields from "../FieldsData";
|
||||
import {TextControl} from "@wordpress/components"; // assuming you're using WordPress components
|
||||
|
||||
const CaptchaKey = ({ field, fields, label }) => {
|
||||
const { getFieldValue, setChangedField, updateField, saveFields} = useFields();
|
||||
|
||||
let fieldValue = getFieldValue(field.id);
|
||||
let captchaVerified = getFieldValue('captcha_fully_enabled');
|
||||
|
||||
const onChangeHandler = async (fieldValue) => {
|
||||
setChangedField(field.id, fieldValue);
|
||||
setChangedField('captcha_fully_enabled', false);
|
||||
updateField(field.id, fieldValue);
|
||||
await saveFields(false, false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextControl
|
||||
required={field.required}
|
||||
placeholder={field.placeholder}
|
||||
help={field.comment}
|
||||
label={label}
|
||||
onChange={(value) => onChangeHandler(value)}
|
||||
value={fieldValue}
|
||||
/>
|
||||
|
||||
<div className="rsssl-email-verified" >
|
||||
{Boolean(captchaVerified)
|
||||
? <Icon name='circle-check' color={'green'} />
|
||||
: <Icon name='circle-times' color={'red'} />
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CaptchaKey;
|
||||
@@ -0,0 +1,46 @@
|
||||
import {useEffect} from "@wordpress/element";
|
||||
const HCaptcha = ({ sitekey, handleCaptchaResponse }) => {
|
||||
const hcaptchaCallback = (response) => {
|
||||
handleCaptchaResponse(response);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const script = document.createElement('script');
|
||||
|
||||
script.src = `https://hcaptcha.com/1/api.js?onload=initHcaptcha`;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
|
||||
script.onload = () => {
|
||||
if (typeof window.hcaptcha !== 'undefined') {
|
||||
window.hcaptcha.render('hcaptchaContainer', {
|
||||
sitekey: sitekey,
|
||||
callback: hcaptchaCallback
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.body.appendChild(script);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
// Check if hcaptcha is loaded before trying to remove it
|
||||
if (window.hcaptcha) {
|
||||
window.hcaptcha.reset();
|
||||
}
|
||||
if (script) {
|
||||
script.remove();
|
||||
}
|
||||
};
|
||||
|
||||
}, [sitekey, handleCaptchaResponse]);
|
||||
|
||||
return (
|
||||
<div className="rsssl-captcha"
|
||||
style={{display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '20px'}}>
|
||||
<div id='hcaptchaContainer'></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HCaptcha;
|
||||
@@ -0,0 +1,62 @@
|
||||
import {useEffect} from "@wordpress/element";
|
||||
import useFields from '../FieldsData';
|
||||
import CaptchaData from "./CaptchaData";
|
||||
|
||||
/**
|
||||
* ReCaptcha functionality.
|
||||
*
|
||||
* @param {function} handleCaptchaResponse - The callback function to handle the ReCaptcha response.
|
||||
* @param {boolean} captchaVerified - Boolean value indicating whether the ReCaptcha is verified or not.
|
||||
* @return {JSX.Element} - The ReCaptcha component JSX.
|
||||
*/
|
||||
const ReCaptcha = ({ handleCaptchaResponse , captchaVerified}) => {
|
||||
const recaptchaCallback = (response) => {
|
||||
handleCaptchaResponse(response);
|
||||
};
|
||||
|
||||
const {reloadCaptcha, removeRecaptchaScript, setReloadCaptcha} = CaptchaData();
|
||||
const {getFieldValue, updateField, saveFields} = useFields();
|
||||
const sitekey = getFieldValue('recaptcha_site_key');
|
||||
const secret = getFieldValue('recaptcha_secret_key');
|
||||
const fully_enabled = getFieldValue('captcha_fully_enabled');
|
||||
|
||||
useEffect(() => {
|
||||
const script = document.createElement('script');
|
||||
|
||||
script.src = `https://www.google.com/recaptcha/api.js?render=explicit&onload=initRecaptcha`;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
|
||||
script.onload = () => {
|
||||
// We restore the recaptcha script if it was not removed.
|
||||
let recaptchaContainer = document.getElementById('recaptchaContainer');
|
||||
if (typeof window.grecaptcha !== 'undefined') {
|
||||
window.initRecaptcha = window.initRecaptcha || (() => {
|
||||
window.grecaptcha && window.grecaptcha.render(recaptchaContainer, {
|
||||
sitekey: sitekey,
|
||||
callback: recaptchaCallback,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.body.appendChild(script);
|
||||
|
||||
}, [sitekey, handleCaptchaResponse]);
|
||||
|
||||
useEffect(() => {
|
||||
// Move cleanup here.
|
||||
if (captchaVerified) {
|
||||
removeRecaptchaScript();
|
||||
}
|
||||
}, [captchaVerified]);
|
||||
|
||||
return (
|
||||
<div className="rsssl-captcha"
|
||||
style={{display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '20px'}} >
|
||||
<div id='recaptchaContainer'></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReCaptcha;
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* The tooltip can't be included in the native toggleControl, so we have to build our own.
|
||||
*/
|
||||
import { useState, useRef, useEffect } from "@wordpress/element";
|
||||
import { __experimentalConfirmDialog as ConfirmDialog } from '@wordpress/components';
|
||||
import hoverTooltip from "../utils/hoverTooltip";
|
||||
import {__} from '@wordpress/i18n';
|
||||
|
||||
const CheckboxControl = (props) => {
|
||||
|
||||
const checkboxRef = useRef(null);
|
||||
|
||||
let disabledCheckboxPropBoolean = (props.disabled === true);
|
||||
let disabledCheckboxViaFieldConfig = (props.field.disabled === true);
|
||||
|
||||
let checkboxDisabled = (
|
||||
disabledCheckboxViaFieldConfig
|
||||
|| disabledCheckboxPropBoolean
|
||||
);
|
||||
|
||||
let tooltipText = '';
|
||||
let emptyValues = [undefined, null, ''];
|
||||
|
||||
if (checkboxDisabled
|
||||
&& props.field.hasOwnProperty('disabledTooltipHoverText')
|
||||
&& !emptyValues.includes(props.field.disabledTooltipHoverText)
|
||||
) {
|
||||
tooltipText = props.field.disabledTooltipHoverText;
|
||||
}
|
||||
|
||||
hoverTooltip(
|
||||
checkboxRef,
|
||||
(checkboxDisabled && (tooltipText !== '')),
|
||||
tooltipText
|
||||
);
|
||||
|
||||
// const tooltipText = __("404 errors detected on your home page. 404 blocking is unavailable, to prevent blocking of legitimate visitors. It is strongly recommended to resolve these errors.", "really-simple-ssl");
|
||||
// // Pass props.disabled as the condition
|
||||
// hoverTooltip(checkboxRef, props.disabled, tooltipText);
|
||||
|
||||
const [ isOpen, setIsOpen ] = useState( false );
|
||||
const onChangeHandler = (e) => {
|
||||
// WordPress <6.0 does not have the confirmdialog component
|
||||
if ( !ConfirmDialog ) {
|
||||
executeAction();
|
||||
return;
|
||||
}
|
||||
if (props.field.warning && props.field.warning.length>0 && !props.field.value) {
|
||||
setIsOpen( true );
|
||||
} else {
|
||||
executeAction();
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
setIsOpen( false );
|
||||
executeAction();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsOpen( false );
|
||||
};
|
||||
|
||||
const executeAction = (e) => {
|
||||
let fieldValue = !props.field.value;
|
||||
props.onChangeHandler(fieldValue)
|
||||
}
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
onChangeHandler(true);
|
||||
}
|
||||
}
|
||||
let field = props.field;
|
||||
let is_checked = field.value ? 'is-checked' : '';
|
||||
let is_disabled = props.disabled ? 'is-disabled' : '';
|
||||
|
||||
return (
|
||||
<>
|
||||
{ConfirmDialog && <ConfirmDialog
|
||||
isOpen={ isOpen }
|
||||
onConfirm={ handleConfirm }
|
||||
onCancel={ handleCancel }
|
||||
>
|
||||
{field.warning}
|
||||
</ConfirmDialog> }
|
||||
<div className="components-base-control components-toggle-control">
|
||||
<div className="components-base-control__field">
|
||||
<div data-wp-component="HStack" className="components-flex components-h-stack">
|
||||
<span className={ "components-form-toggle "+is_checked + ' ' +is_disabled}>
|
||||
<input
|
||||
ref={checkboxRef}
|
||||
onKeyDown={(e) => handleKeyDown(e)}
|
||||
checked={props.value}
|
||||
className="components-form-toggle__input"
|
||||
onChange={ ( e ) => onChangeHandler(e) }
|
||||
id={props.id}
|
||||
type="checkbox"
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
<span className="components-form-toggle__track"></span>
|
||||
<span className="components-form-toggle__thumb"></span>
|
||||
</span>
|
||||
<label htmlFor={field.id} className="components-toggle-control__label">{props.label}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default CheckboxControl
|
||||
@@ -0,0 +1,26 @@
|
||||
.rsssl-datatable-component {
|
||||
|
||||
.rsssl-action-buttons__inner {
|
||||
.rsssl-action-buttons__button {
|
||||
&.rsssl-red {
|
||||
border: 0 solid transparent;
|
||||
background: var(--rsp-red);
|
||||
color: var(--rsp-text-color-white);
|
||||
|
||||
&:hover {
|
||||
background: var(--rsp-dark-red);
|
||||
color: var(--rsp-text-color-white);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rsssl-add-button__button, .rsssl-action-buttons__button {
|
||||
//display: flex;
|
||||
|
||||
.rsssl-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import DataTableStore from "../DataTableStore";
|
||||
import './Buttons.scss'
|
||||
import Icon from "../../../utils/Icon";
|
||||
const ControlButton = ({ controlButton }) => {
|
||||
const {
|
||||
processing,
|
||||
} = DataTableStore();
|
||||
return (
|
||||
<div className="rsssl-add-button">
|
||||
<div className="rsssl-add-button__inner">
|
||||
<button
|
||||
className="button button-secondary button-datatable rsssl-add-button__button"
|
||||
onClick={controlButton.onClick}
|
||||
disabled={processing}
|
||||
>
|
||||
{processing && <Icon name = "loading" color = 'grey' />}
|
||||
{controlButton.label}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ControlButton;
|
||||
@@ -0,0 +1,24 @@
|
||||
import DataTableStore from "../DataTableStore";
|
||||
import './Buttons.scss'
|
||||
import Icon from "../../../utils/Icon";
|
||||
import {memo} from "@wordpress/element";
|
||||
|
||||
const MultiSelectButton = ({ids, buttonData}) => {
|
||||
const {
|
||||
processing,
|
||||
rowAction,
|
||||
} = DataTableStore();
|
||||
return (
|
||||
<div className={`rsssl-action-buttons__inner`}>
|
||||
<button
|
||||
className={`button ${buttonData.className} rsssl-action-buttons__button`}
|
||||
onClick={(e) => rowAction(ids, buttonData.action, buttonData.type, buttonData.reloadFields) }
|
||||
disabled={processing}
|
||||
>
|
||||
{processing && <Icon name = "loading" color = 'grey' />}
|
||||
{buttonData.label}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default memo(MultiSelectButton)
|
||||
@@ -0,0 +1,23 @@
|
||||
import DataTableStore from "../DataTableStore";
|
||||
import './Buttons.scss'
|
||||
import Icon from "../../../utils/Icon";
|
||||
import {memo} from "@wordpress/element";
|
||||
|
||||
const RowButton = ({id, buttonData}) => {
|
||||
const {
|
||||
processing,
|
||||
rowAction,
|
||||
} = DataTableStore();
|
||||
return (
|
||||
<div className={`rsssl-action-buttons__inner`}>
|
||||
<button
|
||||
className={`button ${buttonData.className} rsssl-action-buttons__button`}
|
||||
onClick={(e) => rowAction([id], buttonData.action, buttonData.type, buttonData.reloadFields) }
|
||||
disabled={processing}
|
||||
>
|
||||
{buttonData.label}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default memo(RowButton);
|
||||
@@ -0,0 +1,16 @@
|
||||
//style for checkbox when some rows are selected
|
||||
.rsssl-indeterminate {
|
||||
input[name="select-all-rows"] {
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="10" y="45" width="80" height="10" fill="currentColor"/></svg>') no-repeat center center;
|
||||
}
|
||||
}
|
||||
|
||||
//style for checkbox when all rows are selected
|
||||
.rsssl-all-selected {
|
||||
input[name="select-all-rows"]::before {
|
||||
content: url(data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%3Cpath%20d%3D%27M14.83%204.89l1.34.94-5.81%208.38H9.02L5.78%209.67l1.34-1.25%202.57%202.4z%27%20fill%3D%27%233582c4%27%2F%3E%3C%2Fsvg%3E);
|
||||
margin: -0.1875rem 0 0 -0.25rem;
|
||||
height: 1.3125rem;
|
||||
width: 1.3125rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
.rsssl-datatable-component {
|
||||
margin-left: calc(0px - var(--rsp-spacing-l));
|
||||
margin-right: calc(0px - var(--rsp-spacing-l));
|
||||
>div {
|
||||
//prevent scrollbar on datatable
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.rdt_TableCol, .rdt_TableCell, .rdt_TableCol_Sortable {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.rdt_TableCol:first-child, .rdt_TableCell:first-child {
|
||||
min-width: initial;
|
||||
}
|
||||
|
||||
.rdt_TableHeadRow {
|
||||
.rdt_TableCol:last-child {
|
||||
flex-grow: 0;
|
||||
flex-direction: row-reverse;
|
||||
min-width: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.rdt_TableRow {
|
||||
&:nth-child(odd) {
|
||||
background-color: var(--rsp-grey-200)
|
||||
}
|
||||
|
||||
padding: var(--rsp-spacing-xs) 0;
|
||||
.rdt_TableCell:last-child {
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//wp-core also adds an svg for the select dropdown, so we hide the one from the react datatables component
|
||||
nav.rdt_Pagination > div > svg {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.rsssl-container {
|
||||
padding: 2em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import {produce} from "immer";
|
||||
|
||||
const DataTableStore = create((set, get) => ({
|
||||
|
||||
processing: false,
|
||||
dataLoaded: false,
|
||||
dataActions: {},
|
||||
sourceData: [],
|
||||
filteredData: [],
|
||||
searchTerm:'',
|
||||
searchColumns:[],
|
||||
reloadFields:false,
|
||||
setReloadFields: (reloadFields) => set({reloadFields}),
|
||||
clearAllData: () => set({sourceData: [], filteredData: []}),
|
||||
setProcessing: (processing) => set({processing}),
|
||||
fetchData: async (action, dataActions) => {
|
||||
set({processing: true});
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action,
|
||||
dataActions
|
||||
);
|
||||
if (response && response.data ) {
|
||||
set({filteredData:response.data, sourceData: response.data, dataLoaded: true, processing: false});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
handleSearch: (searchTerm, searchColumns) => {
|
||||
set({searchTerm})
|
||||
set({searchColumns})
|
||||
let data = get().sourceData;
|
||||
const filteredData = data.filter(item =>
|
||||
searchColumns.some(column =>
|
||||
item[column] && item[column].toLowerCase().includes(searchTerm.toLowerCase())
|
||||
));
|
||||
set({filteredData: filteredData});
|
||||
},
|
||||
/*
|
||||
* This function handles the filter, it is called from the GroupSetting class
|
||||
*/
|
||||
handleFilter: async (column, filterValue) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, filterColumn: column, filterValue};
|
||||
})
|
||||
);
|
||||
},
|
||||
restoreView: () => {
|
||||
//filter the data again
|
||||
let searchTerm = get().searchTerm;
|
||||
if ( searchTerm !== '' ) {
|
||||
let searchColumns = get().searchColumns;
|
||||
get().handleSearch(searchTerm, searchColumns);
|
||||
}
|
||||
},
|
||||
//only removes rows from the dataset clientside, does not do an API call
|
||||
removeRows:(ids) => {
|
||||
let filteredData = get().filteredData;
|
||||
let sourceData = get().sourceData;
|
||||
let newFilteredData = filteredData.filter(item => !ids.includes(item.id));
|
||||
let newSourceData = sourceData.filter(item => !ids.includes(item.id));
|
||||
set({filteredData: newFilteredData, sourceData: newSourceData});
|
||||
get().restoreView();
|
||||
},
|
||||
rowAction: async ( ids, action, actionType, reloadFields ) => {
|
||||
actionType = typeof actionType !== 'undefined' ? actionType : '';
|
||||
set({processing: true});
|
||||
if ( actionType === 'delete' ) {
|
||||
get().removeRows(ids);
|
||||
}
|
||||
let data = {
|
||||
ids: ids,
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action,
|
||||
data
|
||||
);
|
||||
|
||||
if ( response.data ) {
|
||||
set({filteredData:response.data, sourceData: response.data, dataLoaded: true, processing: false});
|
||||
get().restoreView();
|
||||
if (reloadFields) {
|
||||
get().setReloadFields(reloadFields);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export default DataTableStore;
|
||||
@@ -0,0 +1,176 @@
|
||||
import {useEffect, useState , memo } from "@wordpress/element";
|
||||
import DataTable, { createTheme } from "react-data-table-component";
|
||||
|
||||
import DataTableStore from "../DataTable/DataTableStore";
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import ControlButton from "../DataTable/Buttons/ControlButton";
|
||||
import RowButton from "../DataTable/Buttons/RowButton";
|
||||
import SearchBar from "../DataTable/SearchBar/SearchBar";
|
||||
import SelectedRowsControl from "../DataTable/SelectedRowsControl/SelectedRowsControl";
|
||||
import './DataTable.scss';
|
||||
import './Checkboxes.scss';
|
||||
import useFields from "../FieldsData";
|
||||
import useMenu from "../../Menu/MenuData";
|
||||
|
||||
const DataTableWrapper = ({field, controlButton, enabled}) => {
|
||||
const {
|
||||
filteredData,
|
||||
handleSearch,
|
||||
dataLoaded,
|
||||
fetchData,
|
||||
reloadFields,
|
||||
} = DataTableStore();
|
||||
const {fetchFieldsData} = useFields();
|
||||
const {selectedSubMenuItem} = useMenu();
|
||||
const [rowsSelected, setRowsSelected] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
|
||||
useEffect(() => {
|
||||
if ( !dataLoaded) {
|
||||
fetchData(field.action, {});
|
||||
}
|
||||
}, [dataLoaded] );
|
||||
|
||||
useEffect(() => {
|
||||
if ( reloadFields ) {
|
||||
fetchFieldsData(selectedSubMenuItem);
|
||||
}
|
||||
}, [reloadFields]);
|
||||
|
||||
/**
|
||||
* Build a column configuration object.
|
||||
*
|
||||
* @param {object} column - The column object.
|
||||
* @param {string} column.name - The name of the column.
|
||||
* @param {boolean} column.sortable - Whether the column is sortable.
|
||||
* @param {boolean} column.searchable - Whether the column is searchable.
|
||||
* @param {number} column.width - The width of the column.
|
||||
* @param {boolean} column.visible - Whether the column is visible.
|
||||
* @param {string} column.column - The column identifier.
|
||||
*
|
||||
* @returns {object} The column configuration object.
|
||||
*/
|
||||
const buildColumn = ({reloadFields, name, isButton, action, label, className, sortable, searchable, width, visible, column}) => ({
|
||||
reloadFields, name, isButton, action, label, className, sortable, searchable, width, visible, column, selector: row => row[column],
|
||||
});
|
||||
const columns = field.columns.map(buildColumn);
|
||||
const buttonColumns = columns.filter(column => column.isButton);
|
||||
const hasSelectableRows = buttonColumns.length>0;
|
||||
const searchableColumns = columns.filter(column => column.searchable).map(column => column.column);
|
||||
|
||||
const customgitStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light');
|
||||
|
||||
const handleSelection = ({selectedCount, selectedRows}) => {
|
||||
// based on the current page and the rows per page we get the rows that are selected
|
||||
let actualRows = rowsPerPage;
|
||||
//in case not all selected, get the rows that are selected from the current page.
|
||||
//the datatable component selects 'all' rows, but we only want the rows from the current page.
|
||||
let rows = [];
|
||||
if ( selectedCount < rowsPerPage ) {
|
||||
rows = selectedRows;
|
||||
setRowsSelected(selectedRows);
|
||||
} else if ( selectedCount >= rowsPerPage ) {
|
||||
//previously all rows were selected, but now some were unselected.
|
||||
//in the latter case we need to get the rows that are selected from the current page.
|
||||
//remove the rows from all pages after the current page
|
||||
let diff = filteredData.length - selectedRows.length;
|
||||
rows = selectedRows.slice( 0, (currentPage * rowsPerPage) - diff );
|
||||
if ( currentPage > 1 ) {
|
||||
//remove the rows from all pages before the current page from the selected rows
|
||||
rows = rows.slice( (currentPage - 1) * rowsPerPage);
|
||||
}
|
||||
setRowsSelected(rows);
|
||||
}
|
||||
}
|
||||
|
||||
const data= dataLoaded && filteredData.length>0 ? {...filteredData} : [];
|
||||
for (const key in data) {
|
||||
const dataItem = {...data[key]};
|
||||
//check if there exists a column with column = 'actionButton'
|
||||
if ( buttonColumns.length > 0 ) {
|
||||
for (const buttonColumn of buttonColumns) {
|
||||
dataItem[buttonColumn.column] = <RowButton id={dataItem.id} buttonData={buttonColumn}/>
|
||||
}
|
||||
}
|
||||
data[key] = dataItem;
|
||||
}
|
||||
let selectAllRowsClass = "";
|
||||
if ( rowsSelected.length>0 && rowsSelected.length < rowsPerPage) {
|
||||
selectAllRowsClass = "rsssl-indeterminate";
|
||||
}
|
||||
if ( rowsSelected.length === rowsPerPage ) {
|
||||
selectAllRowsClass = "rsssl-all-selected";
|
||||
}
|
||||
return (
|
||||
<div className={"rsssl-datatable-component"}>
|
||||
<div className="rsssl-container">
|
||||
{controlButton.show && <ControlButton controlButton={controlButton}/> }
|
||||
{/*Ensure that positioning also works without the addButton, by adding a div */}
|
||||
{ !controlButton.show && <div></div>}
|
||||
<SearchBar
|
||||
handleSearch={handleSearch}
|
||||
searchableColumns={searchableColumns}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ field.multiselect_buttons && rowsSelected.length > 0 && (
|
||||
<SelectedRowsControl rowsSelected={rowsSelected} buttonData = {field.multiselect_buttons} />
|
||||
)}
|
||||
|
||||
<DataTable
|
||||
className={ selectAllRowsClass }
|
||||
columns={columns}
|
||||
data={Object.values(data)}
|
||||
dense
|
||||
pagination={true}
|
||||
paginationComponentOptions={{
|
||||
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
|
||||
rangeSeparatorText: __('of', 'really-simple-ssl'),
|
||||
noRowsPerPage: false,
|
||||
selectAllRowsItem: false,
|
||||
selectAllRowsItemText: __('All', 'really-simple-ssl'),
|
||||
}}
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
selectableRows={hasSelectableRows}
|
||||
//clearSelectedRows={() => setRowsSelected([])}
|
||||
paginationPerPage={rowsPerPage}
|
||||
onChangePage={setCurrentPage}
|
||||
onChangeRowsPerPage={setRowsPerPage}
|
||||
onSelectedRowsChange={handleSelection}
|
||||
theme="really-simple-plugins"
|
||||
// customStyles={customStyles}
|
||||
/>
|
||||
{!enabled && (
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay">
|
||||
<span className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span>
|
||||
<span>{__('Here you can add IP addresses that should never be blocked by region restrictions.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(DataTableWrapper);
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import './SearchBar.scss';
|
||||
import {memo} from "@wordpress/element";
|
||||
|
||||
const SearchBar = ({ handleSearch, searchableColumns }) => {
|
||||
const [debounceTimer, setDebounceTimer] = useState(null);
|
||||
|
||||
const onKeyUp = (event) => {
|
||||
clearTimeout(debounceTimer);
|
||||
setDebounceTimer(setTimeout(() => {
|
||||
handleSearch(event.target.value, searchableColumns)
|
||||
}, 500));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rsssl-search-bar">
|
||||
<div className="rsssl-search-bar__inner">
|
||||
<div className="rsssl-search-bar__icon"></div>
|
||||
<input
|
||||
type="text"
|
||||
className="rsssl-search-bar__input"
|
||||
placeholder={__("Search", "really-simple-ssl")}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(SearchBar);
|
||||
@@ -0,0 +1,32 @@
|
||||
.rsssl-search-bar {
|
||||
float: right;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.rsssl-search-bar__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.rsssl-search-bar__inner:focus-within {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.rsssl-search-bar__icon {
|
||||
/* Add styles for the search icon */
|
||||
}
|
||||
|
||||
.rsssl-search-bar__input {
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 3px 5px;
|
||||
width: 150px; /* Adjust width as needed */
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.rsssl-search-bar__input:focus {
|
||||
width: 200px; /* Adjust width as needed */
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import {__, _n} from "@wordpress/i18n";
|
||||
import DataTableStore from "../DataTableStore";
|
||||
import MultiSelectButton from "../Buttons/MultiSelectButton";
|
||||
import './SelectedRowsControl.scss'
|
||||
import {memo} from "@wordpress/element";
|
||||
import MenuItem from "../../../Menu/MenuItem";
|
||||
|
||||
const SelectedRowsControl = ({ rowsSelected, buttonData }) => {
|
||||
const {
|
||||
processing,
|
||||
filteredData,
|
||||
} = DataTableStore();
|
||||
//ensure that all items in the rowsSelected array still exist in the filteredData array
|
||||
//after a delete this might not be the case
|
||||
let rowsSelectedFiltered = rowsSelected.filter(selectedRow =>
|
||||
filteredData.some(filteredRow => filteredRow.id === selectedRow.id)
|
||||
);
|
||||
|
||||
if ( rowsSelectedFiltered.length === 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//parse ids from rowsSelected into array
|
||||
const ids = rowsSelectedFiltered.map((row) => row.id);
|
||||
return (
|
||||
<div className="rsssl-selected-rows-control">
|
||||
<div className={"rsssl-multiselect-datatable-form rsssl-primary"}>
|
||||
<div>
|
||||
{_n( "You have selected %d row", "You have selected %d rows", rowsSelectedFiltered.length, 'really-simple-ssl' ).replace('%d', rowsSelectedFiltered.length )}
|
||||
</div>
|
||||
<div className="rsssl-action-buttons">
|
||||
<>
|
||||
{ buttonData.map((buttonItem, i) => <MultiSelectButton key={"multiselectButton-"+i} ids={ids} buttonData={buttonItem} /> ) }
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(SelectedRowsControl);
|
||||
@@ -0,0 +1,14 @@
|
||||
.rsssl-selected-rows-control {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
|
||||
//blue container above datatable for multiselect
|
||||
.rsssl-multiselect-datatable-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
Justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 1em 2em;
|
||||
background: var(--rsp-blue-faded);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
const AddButton = ({ getCurrentFilter, moduleName, handleOpen, processing, blockedText, allowedText }) => {
|
||||
let buttonText = getCurrentFilter(moduleName) === 'blocked' ? blockedText : allowedText;
|
||||
|
||||
return (
|
||||
<div className="rsssl-add-button">
|
||||
{(getCurrentFilter(moduleName) === 'blocked' || getCurrentFilter(moduleName) === 'allowed') && (
|
||||
<div className="rsssl-add-button__inner">
|
||||
<button
|
||||
className="button button-secondary button-datatable rsssl-add-button__button"
|
||||
onClick={handleOpen}
|
||||
disabled={processing}
|
||||
>
|
||||
{buttonText}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddButton;
|
||||
@@ -0,0 +1,178 @@
|
||||
import {__} from '@wordpress/i18n';
|
||||
import {useRef, useEffect, useState} from '@wordpress/element';
|
||||
import DataTable, {createTheme} from "react-data-table-component";
|
||||
import useFields from "../FieldsData";
|
||||
import DynamicDataTableStore from "./DynamicDataTableStore";
|
||||
|
||||
const DynamicDataTable = (props) => {
|
||||
const {
|
||||
twoFAMethods,
|
||||
setTwoFAMethods,
|
||||
DynamicDataTable,
|
||||
dataLoaded,
|
||||
pagination,
|
||||
dataActions,
|
||||
handleTableRowsChange,
|
||||
fetchDynamicData,
|
||||
// setDynamicData,
|
||||
handleTableSort,
|
||||
handleTablePageChange,
|
||||
handleTableSearch,
|
||||
} = DynamicDataTableStore();
|
||||
|
||||
let field = props.field;
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const {fields, getFieldValue, saveFields} = useFields();
|
||||
|
||||
const twoFAEnabledRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
twoFAEnabledRef.current = getFieldValue('login_protection_enabled');
|
||||
saveFields(true, false)
|
||||
}, [getFieldValue('login_protection_enabled')]);
|
||||
|
||||
useEffect(() => {
|
||||
const value = getFieldValue('login_protection_enabled');
|
||||
setEnabled(value);
|
||||
}, [fields]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dataLoaded || enabled !== getFieldValue('login_protection_enabled')) {
|
||||
fetchDynamicData(field.action)
|
||||
.then(response => {
|
||||
// Check if response.data is defined and is an array before calling reduce
|
||||
if(response.data && Array.isArray(response.data)) {
|
||||
const methods = response.data.reduce((acc, user) => ({...acc, [user.id]: user.rsssl_two_fa_status}), {});
|
||||
setTwoFAMethods(methods);
|
||||
} else {
|
||||
console.error('Unexpected response:', response);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err); // Log any errors
|
||||
});
|
||||
}
|
||||
}, [dataLoaded, field.action, fetchDynamicData, getFieldValue('login_protection_enabled')]); // Add getFieldValue('login_protection_enabled') as a dependency
|
||||
|
||||
useEffect(() => {
|
||||
if (dataActions) {
|
||||
fetchDynamicData(field.action);
|
||||
}
|
||||
}, [dataActions]);
|
||||
|
||||
function buildColumn(column) {
|
||||
let newColumn = {
|
||||
name: column.name,
|
||||
column: column.column,
|
||||
sortable: column.sortable,
|
||||
searchable: column.searchable,
|
||||
width: column.width,
|
||||
visible: column.visible,
|
||||
selector: row => row[column.column],
|
||||
};
|
||||
|
||||
return newColumn;
|
||||
}
|
||||
|
||||
let columns = [];
|
||||
|
||||
field.columns.forEach(function (item, i) {
|
||||
let newItem = { ...item, key: item.column };
|
||||
newItem = buildColumn(newItem);
|
||||
newItem.visible = newItem.visible ?? true;
|
||||
columns.push(newItem);
|
||||
});
|
||||
|
||||
let searchableColumns = columns
|
||||
.filter(column => column.searchable)
|
||||
.map(column => column.column);
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-search-bar">
|
||||
<div className="rsssl-search-bar__inner">
|
||||
<div className="rsssl-search-bar__icon"></div>
|
||||
<input
|
||||
type="text"
|
||||
className="rsssl-search-bar__input"
|
||||
placeholder={__("Search", "really-simple-ssl")}
|
||||
onChange={event => handleTableSearch(event.target.value, searchableColumns)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{dataLoaded ?
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={DynamicDataTable}
|
||||
dense
|
||||
pagination
|
||||
paginationServer
|
||||
onChangeRowsPerPage={handleTableRowsChange}
|
||||
onChangePage={handleTablePageChange}
|
||||
sortServer
|
||||
onSort={handleTableSort}
|
||||
paginationRowsPerPageOptions={[10, 25, 50, 100]}
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
theme="really-simple-plugins"
|
||||
customStyles={customStyles}
|
||||
></DataTable>
|
||||
:
|
||||
<div className="rsssl-spinner" style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: "100px"
|
||||
}}>
|
||||
<div className="rsssl-spinner__inner">
|
||||
<div className="rsssl-spinner__icon" style={{
|
||||
border: '8px solid white',
|
||||
borderTop: '8px solid #f4bf3e',
|
||||
borderRadius: '50%',
|
||||
width: '120px',
|
||||
height: '120px',
|
||||
animation: 'spin 2s linear infinite'
|
||||
}}></div>
|
||||
<div className="rsssl-spinner__text" style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}>{__("Loading data, please stand by...", "really-simple-ssl")}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{ !enabled &&
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay"><span
|
||||
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Activate Two-Factor Authentication to enable this block.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
|
||||
}
|
||||
export default DynamicDataTable;
|
||||
@@ -0,0 +1,77 @@
|
||||
/* Creates A Store For Risk Data using Zustand */
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import {produce} from "immer";
|
||||
import React, {useState} from "react";
|
||||
|
||||
const DynamicDataTableStore = create((set, get) => ({
|
||||
|
||||
twoFAMethods: {},
|
||||
setTwoFAMethods: (methods) => set((state) => ({ ...state, twoFAMethods: methods })),
|
||||
processing: false,
|
||||
dataLoaded: false,
|
||||
pagination: {},
|
||||
dataActions: {},
|
||||
DynamicDataTable: [],
|
||||
fetchDynamicData: async (action) => {
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action,
|
||||
get().dataActions
|
||||
);
|
||||
let data = Array.isArray(response.data) ? response.data : [];
|
||||
let pagination = response.pagination ? response.pagination : 1;
|
||||
//now we set the EventLog
|
||||
if ( response ) {
|
||||
set(state => ({
|
||||
...state,
|
||||
DynamicDataTable: data,
|
||||
dataLoaded: true,
|
||||
processing: false,
|
||||
pagination: pagination,
|
||||
// Removed the twoFAMethods set from here...
|
||||
}));
|
||||
// Return the response for the calling function to use
|
||||
return response;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
|
||||
handleTableSearch: async (search, searchColumns) => {
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, search, searchColumns};
|
||||
}));
|
||||
},
|
||||
|
||||
|
||||
handleTablePageChange: async (page, pageSize) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, page, pageSize};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
handleTableRowsChange: async (currentRowsPerPage, currentPage) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, currentRowsPerPage, currentPage};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
//this handles all pagination and sorting
|
||||
handleTableSort: async (column, sortDirection) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, sortColumn: column, sortDirection};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
export default DynamicDataTableStore;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
const SearchBar = ({ handleSearch, searchableColumns }) => {
|
||||
const [debounceTimer, setDebounceTimer] = useState(null);
|
||||
|
||||
const onKeyUp = (event) => {
|
||||
clearTimeout(debounceTimer);
|
||||
setDebounceTimer(setTimeout(() => {
|
||||
handleSearch(event.target.value, searchableColumns)
|
||||
}, 500));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rsssl-search-bar">
|
||||
<div className="rsssl-search-bar__inner">
|
||||
<div className="rsssl-search-bar__icon"></div>
|
||||
<input
|
||||
type="text"
|
||||
className="rsssl-search-bar__input"
|
||||
placeholder={__("Search", "really-simple-ssl")}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchBar;
|
||||
@@ -0,0 +1,253 @@
|
||||
import {__} from '@wordpress/i18n';
|
||||
import {useEffect, useState} from '@wordpress/element';
|
||||
import DataTable, {createTheme} from "react-data-table-component";
|
||||
import EventLogDataTableStore from "./EventLogDataTableStore";
|
||||
import FilterData from "../FilterData";
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import useMenu from "../../Menu/MenuData";
|
||||
import Flag from "../../utils/Flag/Flag";
|
||||
import Icon from "../../utils/Icon";
|
||||
import useFields from "../FieldsData";
|
||||
import SearchBar from "../DynamicDataTable/SearchBar";
|
||||
|
||||
const EventLogDataTable = (props) => {
|
||||
const {
|
||||
DynamicDataTable,
|
||||
dataLoaded,
|
||||
pagination,
|
||||
dataActions,
|
||||
handleEventTableRowsChange,
|
||||
fetchDynamicData,
|
||||
handleEventTableSort,
|
||||
handleEventTablePageChange,
|
||||
handleEventTableSearch,
|
||||
handleEventTableFilter,
|
||||
processing,
|
||||
rowCleared,
|
||||
} = EventLogDataTableStore()
|
||||
//here we set the selectedFilter from the Settings group
|
||||
const {
|
||||
selectedFilter,
|
||||
setSelectedFilter,
|
||||
activeGroupId,
|
||||
getCurrentFilter,
|
||||
setProcessingFilter,
|
||||
} = FilterData();
|
||||
|
||||
|
||||
|
||||
const {fields, fieldAlreadyEnabled, getFieldValue} = useFields();
|
||||
const [tableHeight, setTableHeight] = useState(600); // Starting height
|
||||
const rowHeight = 50; // Height of each row.
|
||||
const moduleName = 'rsssl-group-filter-' + props.field.id;
|
||||
let field = props.field;
|
||||
useEffect(() => {
|
||||
const currentFilter = getCurrentFilter(moduleName);
|
||||
|
||||
if (!currentFilter) {
|
||||
setSelectedFilter('all', moduleName);
|
||||
}
|
||||
setProcessingFilter(processing);
|
||||
handleEventTableFilter('severity', currentFilter);
|
||||
}, [moduleName, handleEventTableFilter, getCurrentFilter(moduleName), setSelectedFilter, moduleName, DynamicDataTable, processing]);
|
||||
|
||||
|
||||
//if the dataActions are changed, we fetch the data
|
||||
useEffect(() => {
|
||||
//we make sure the dataActions are changed in the store before we fetch the data
|
||||
if (dataActions) {
|
||||
fetchDynamicData(field.action, field.event_type, dataActions)
|
||||
}
|
||||
}, [dataActions.sortDirection, dataActions.filterValue, dataActions.search, dataActions.page, dataActions.currentRowsPerPage]);
|
||||
|
||||
|
||||
|
||||
//we create the columns
|
||||
let columns = [];
|
||||
//getting the fields from the props
|
||||
|
||||
//we loop through the fields
|
||||
field.columns.forEach(function (item, i) {
|
||||
let newItem = buildColumn(item)
|
||||
columns.push(newItem);
|
||||
});
|
||||
|
||||
let enabled = fieldAlreadyEnabled('event_log_enabled');
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for head cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for data cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light');
|
||||
|
||||
//only show the datatable if the data is loaded
|
||||
if (!dataLoaded && columns.length === 0 && DynamicDataTable.length === 0) {
|
||||
return (
|
||||
<div className="rsssl-spinner">
|
||||
<div className="rsssl-spinner__inner">
|
||||
<div className="rsssl-spinner__icon"></div>
|
||||
<div className="rsssl-spinner__text">{__("Loading...", "really-simple-ssl")}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let searchableColumns = [];
|
||||
//setting the searchable columns
|
||||
columns.map(column => {
|
||||
if (column.searchable) {
|
||||
searchableColumns.push(column.column);
|
||||
}
|
||||
});
|
||||
let data = [];
|
||||
|
||||
if (DynamicDataTable.data) {
|
||||
data = DynamicDataTable.data.map((dataItem) => {
|
||||
let newItem = {...dataItem};
|
||||
newItem.iso2_code = generateFlag(newItem.iso2_code, newItem.country_name);
|
||||
newItem.expandableRows = true;
|
||||
return newItem;
|
||||
});
|
||||
}
|
||||
|
||||
//we generate an expandable row
|
||||
const ExpandableRow = ({data}) => {
|
||||
let code, icon, color = '';
|
||||
switch (data.severity) {
|
||||
case 'warning':
|
||||
code = 'rsssl-warning';
|
||||
icon = 'circle-times';
|
||||
color = 'red';
|
||||
break;
|
||||
case 'informational':
|
||||
code = 'rsssl-primary';
|
||||
icon = 'info';
|
||||
color = 'black';
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
code = 'rsssl-primary';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"rsssl-wizard-help-notice " + code}
|
||||
style={{padding: '1em', borderRadius: '5px'}}>
|
||||
{/*now we place a block to the rightcorner with the severity*/}
|
||||
<div style={{float: 'right'}}>
|
||||
<Icon name={icon} color={color}/>
|
||||
</div>
|
||||
<div style={{fontSize: '1em', fontWeight: 'bold'}}>
|
||||
{data.severity.charAt(0).toUpperCase() + data.severity.slice(1)}
|
||||
</div>
|
||||
<div>{data.description}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
function generateFlag(flag, title) {
|
||||
return (
|
||||
<>
|
||||
<Flag
|
||||
countryCode={flag}
|
||||
style={{
|
||||
fontSize: '2em',
|
||||
marginLeft: '0.3em',
|
||||
}}
|
||||
title={title}
|
||||
></Flag>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
let paginationSet;
|
||||
paginationSet = typeof pagination !== 'undefined';
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(data).length === 0 ) {
|
||||
setTableHeight(100); // Adjust depending on your UI measurements
|
||||
} else {
|
||||
setTableHeight(rowHeight * (paginationSet ? pagination.perPage + 2 : 12)); // Adjust depending on your UI measurements
|
||||
}
|
||||
|
||||
}, [paginationSet, pagination?.perPage, data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-container">
|
||||
<div></div>
|
||||
{/*Display the search bar*/}
|
||||
<SearchBar handleSearch={handleEventTableSearch} searchableColumns={searchableColumns}/>
|
||||
</div>
|
||||
{/*Display the datatable*/}
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={processing? [] : data}
|
||||
dense
|
||||
pagination={!processing}
|
||||
paginationServer
|
||||
paginationTotalRows={paginationSet? pagination.totalRows: 10}
|
||||
paginationPerPage={paginationSet? pagination.perPage: 10}
|
||||
paginationDefaultPage={paginationSet?pagination.currentPage: 1}
|
||||
paginationComponentOptions={{
|
||||
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
|
||||
rangeSeparatorText: __('of', 'really-simple-ssl'),
|
||||
noRowsPerPage: false,
|
||||
selectAllRowsItem: false,
|
||||
selectAllRowsItemText: __('All', 'really-simple-ssl'),
|
||||
|
||||
}}
|
||||
onChangeRowsPerPage={handleEventTableRowsChange}
|
||||
onChangePage={handleEventTablePageChange}
|
||||
expandableRows={!processing}
|
||||
expandableRowsComponent={ExpandableRow}
|
||||
loading={dataLoaded}
|
||||
onSort={handleEventTableSort}
|
||||
sortServer={!processing}
|
||||
paginationRowsPerPageOptions={[5, 10, 25, 50, 100]}
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
theme="really-simple-plugins"
|
||||
customStyles={customStyles}
|
||||
></DataTable>
|
||||
{!enabled && (
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay"><span
|
||||
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Activate Limit login attempts to enable this block.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
export default EventLogDataTable;
|
||||
|
||||
function buildColumn(column) {
|
||||
return {
|
||||
name: column.name,
|
||||
sortable: column.sortable,
|
||||
searchable: column.searchable,
|
||||
width: column.width,
|
||||
visible: column.visible,
|
||||
column: column.column,
|
||||
selector: row => row[column.column],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/* Creates A Store For Risk Data using Zustand */
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import {produce} from "immer";
|
||||
import React from "react";
|
||||
|
||||
const EventLogDataTableStore = create((set, get) => ({
|
||||
|
||||
processing: false,
|
||||
dataLoaded: false,
|
||||
pagination: {},
|
||||
dataActions: {},
|
||||
DynamicDataTable: [],
|
||||
sorting: [],
|
||||
rowCleared: false,
|
||||
|
||||
fetchDynamicData: async (action, event_type, dataActions = {}) => {
|
||||
//cool we can fetch the data so first we set the processing to true
|
||||
set({processing: true});
|
||||
set({dataLoaded: false});
|
||||
set({rowCleared: true});
|
||||
if (Object.keys(dataActions).length === 0) {
|
||||
dataActions = get().dataActions;
|
||||
}
|
||||
// add the data_type to the dataActions
|
||||
dataActions = {...dataActions, event_type};
|
||||
//now we fetch the data
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action,
|
||||
dataActions
|
||||
);
|
||||
// now we set the EventLog
|
||||
if (response) {
|
||||
set({DynamicDataTable: response, dataLoaded: true, processing: false, pagination: response.pagination, sorting: response.sorting});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
set({processing: false});
|
||||
set({rowCleared: false});
|
||||
}
|
||||
},
|
||||
|
||||
handleEventTableSearch: async (search, searchColumns) => {
|
||||
//Add the search to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, search, searchColumns};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
handleEventTablePageChange: async (page, pageSize) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, page, pageSize};
|
||||
})
|
||||
);
|
||||
},
|
||||
handleEventTableRowsChange: async (currentRowsPerPage, currentPage) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, currentRowsPerPage, currentPage};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
//this handles all pagination and sorting
|
||||
handleEventTableSort: async (column, sortDirection) => {
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, sortColumn: column, sortDirection};
|
||||
})
|
||||
);
|
||||
},
|
||||
handleEventTableFilter: async (column, filterValue) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, filterColumn: column, filterValue};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
}));
|
||||
|
||||
export default EventLogDataTableStore;
|
||||
@@ -0,0 +1,625 @@
|
||||
import {
|
||||
TextControl,
|
||||
RadioControl,
|
||||
TextareaControl,
|
||||
__experimentalNumberControl as NumberControl
|
||||
} from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import License from "./License/License";
|
||||
import Password from "./Password";
|
||||
import SelectControl from "./SelectControl";
|
||||
import Host from "./Host/Host";
|
||||
import Hyperlink from "../utils/Hyperlink";
|
||||
import LetsEncrypt from "../LetsEncrypt/LetsEncrypt";
|
||||
import Activate from "../LetsEncrypt/Activate";
|
||||
import MixedContentScan from "./MixedContentScan/MixedContentScan";
|
||||
import PermissionsPolicy from "./PermissionsPolicy";
|
||||
import CheckboxControl from "./CheckboxControl";
|
||||
import Support from "./Support";
|
||||
import LearningMode from "./LearningMode/LearningMode";
|
||||
import RiskComponent from "./RiskConfiguration/RiskComponent";
|
||||
import VulnerabilitiesOverview from "./RiskConfiguration/vulnerabilitiesOverview";
|
||||
import IpAddressDatatable from "./LimitLoginAttempts/IpAddressDatatable";
|
||||
import TwoFaRolesDropDown from "./TwoFA/TwoFaRolesDropDown";
|
||||
import Button from "./Button";
|
||||
import Icon from "../utils/Icon";
|
||||
import { useEffect, useState, useRef } from "@wordpress/element";
|
||||
import useFields from "./FieldsData";
|
||||
import PostDropdown from "./PostDropDown";
|
||||
import NotificationTester from "./RiskConfiguration/NotificationTester";
|
||||
import getAnchor from "../utils/getAnchor";
|
||||
import useMenu from "../Menu/MenuData";
|
||||
import UserDatatable from "./LimitLoginAttempts/UserDatatable";
|
||||
import CountryDatatable from "./LimitLoginAttempts/CountryDatatable";
|
||||
import BlockListDatatable from "./GeoBlockList/BlockListDatatable";
|
||||
import TwoFaDataTable from "./TwoFA/TwoFaDataTable";
|
||||
import EventLogDataTable from "./EventLog/EventLogDataTable";
|
||||
import DOMPurify from "dompurify";
|
||||
import RolesDropDown from "./RolesDropDown";
|
||||
import GeoDatatable from "./GeoBlockList/GeoDatatable";
|
||||
import WhiteListDatatable from "./GeoBlockList/WhiteListDatatable";
|
||||
import Captcha from "./Captcha/Captcha";
|
||||
import CaptchaKey from "./Captcha/CaptchaKey";
|
||||
import FileChangeDetection from "./FileChangeDetection/FileChangeDetection";
|
||||
import UserAgentTable from "./firewall/UserAgentTable";
|
||||
import TwoFaEnabledDropDown from "./TwoFA/TwoFaEnabledDropDown";
|
||||
const Field = (props) => {
|
||||
const scrollAnchor = useRef(null);
|
||||
const { updateField, setChangedField, highLightField, setHighLightField , getFieldValue} = useFields();
|
||||
const [anchor, setAnchor] = useState(null);
|
||||
const { selectedFilter, setSelectedFilter } = useMenu();
|
||||
|
||||
useEffect(() => {
|
||||
// Check URL for anchor and highlightfield parameters
|
||||
const anchor = getAnchor('anchor');
|
||||
const highlightField = getAnchor('highlightfield');
|
||||
|
||||
setAnchor(anchor);
|
||||
|
||||
if (highlightField) {
|
||||
setHighLightField(highlightField);
|
||||
}
|
||||
|
||||
if (highlightField === props.field.id && scrollAnchor.current) {
|
||||
scrollAnchor.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
//if the field is a captcha provider, scroll to the captcha provider is a temp fix cause i can't get the scroll to work properly.
|
||||
if (highLightField === 'enabled_captcha_provider' && props.fields) {
|
||||
let captchaField = document.getElementsByClassName('rsssl-highlight')[0];
|
||||
if (captchaField) {
|
||||
captchaField.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
|
||||
if (anchor && anchor === props.field.id) {
|
||||
scrollAnchor.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
handleAnchor();
|
||||
}, [anchor]);
|
||||
|
||||
window.addEventListener('hashchange', () => {
|
||||
const anchor = getAnchor('anchor');
|
||||
const highlightField = getAnchor('highlightfield');
|
||||
|
||||
setAnchor(anchor);
|
||||
|
||||
if (highlightField) {
|
||||
setHighLightField(highlightField);
|
||||
}
|
||||
|
||||
if (highlightField === props.field.id && scrollAnchor.current) {
|
||||
scrollAnchor.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
if (anchor && anchor === props.field.id) {
|
||||
scrollAnchor.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
});
|
||||
|
||||
const handleAnchor = () => {
|
||||
if (anchor && anchor === props.field.id) {
|
||||
scrollAnchor.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeHandler = (fieldValue) => {
|
||||
let field = props.field;
|
||||
if (field.pattern) {
|
||||
const regex = new RegExp(field.pattern, 'g');
|
||||
const allowedCharactersArray = fieldValue.match(regex);
|
||||
fieldValue = allowedCharactersArray ? allowedCharactersArray.join('') : '';
|
||||
}
|
||||
updateField(field.id, fieldValue);
|
||||
|
||||
// We can configure other fields if a field is enabled, or set to a certain value.
|
||||
let configureFieldCondition = false;
|
||||
if (field.configure_on_activation) {
|
||||
if (field.configure_on_activation.hasOwnProperty('condition') && props.field.value == field.configure_on_activation.condition) {
|
||||
configureFieldCondition = true;
|
||||
}
|
||||
let configureField = field.configure_on_activation[0];
|
||||
for (let fieldId in configureField) {
|
||||
if (configureFieldCondition && configureField.hasOwnProperty(fieldId)) {
|
||||
updateField(fieldId, configureField[fieldId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setChangedField(field.id, fieldValue);
|
||||
};
|
||||
|
||||
const labelWrap = (field) => {
|
||||
let tooltipColor = field.warning ? 'red' : 'black';
|
||||
return (
|
||||
<>
|
||||
<div className="cmplz-label-text">{field.label}</div>
|
||||
{field.tooltip && <Icon name="info-open" tooltip={field.tooltip} color={tooltipColor} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
let field = props.field;
|
||||
let fieldValue = field.value;
|
||||
let disabled = field.disabled;
|
||||
let highLightClass = 'rsssl-field-wrap';
|
||||
if (highLightField === props.field.id) {
|
||||
highLightClass = 'rsssl-field-wrap rsssl-highlight';
|
||||
}
|
||||
|
||||
let options = [];
|
||||
if (field.options) {
|
||||
for (let key in field.options) {
|
||||
if (field.options.hasOwnProperty(key)) {
|
||||
let item = {};
|
||||
item.label = field.options[key];
|
||||
item.value = key;
|
||||
options.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a feature can only be used on networkwide or single site setups, pass that info here.
|
||||
if (!rsssl_settings.networkwide_active && field.networkwide_required) {
|
||||
disabled = true;
|
||||
field.comment = (
|
||||
<>
|
||||
{__("This feature is only available networkwide.", "really-simple-ssl")}
|
||||
<Hyperlink target="_blank" rel="noopener noreferrer" text={__("Network settings", "really-simple-ssl")} url={rsssl_settings.network_link} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.conditionallyDisabled) {
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
if (!field.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( field.type==='checkbox' ) {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<CheckboxControl
|
||||
label={labelWrap(field)}
|
||||
field={field}
|
||||
disabled={disabled}
|
||||
onChangeHandler={ ( fieldValue ) => onChangeHandler( fieldValue ) }
|
||||
/>
|
||||
{ field.comment &&
|
||||
<div className="rsssl-comment" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(field.comment) }} />
|
||||
/* nosemgrep: react-dangerouslysetinnerhtml */
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ( field.type==='hidden' ){
|
||||
return (
|
||||
<input type="hidden" value={field.value}/>
|
||||
);
|
||||
}
|
||||
|
||||
if ( field.type==='radio' ){
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<RadioControl
|
||||
label={labelWrap(field)}
|
||||
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
|
||||
selected={ fieldValue }
|
||||
options={ options }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type==='email'){
|
||||
const sendVerificationEmailField = props.fields.find(field => field.id === 'send_verification_email');
|
||||
const emailIsVerified = sendVerificationEmailField && (sendVerificationEmailField.disabled === false);
|
||||
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor} style={{position: 'relative'}}>
|
||||
<TextControl
|
||||
required={ field.required }
|
||||
placeholder={ field.placeholder }
|
||||
disabled={ disabled }
|
||||
help={ field.comment }
|
||||
label={labelWrap(field)}
|
||||
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
|
||||
value= { fieldValue }
|
||||
/>
|
||||
{ sendVerificationEmailField &&
|
||||
<div className="rsssl-email-verified" >
|
||||
{!emailIsVerified
|
||||
? <Icon name='circle-check' color={'green'} />
|
||||
: <Icon name='circle-times' color={'red'} />}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type==='captcha_key') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor} style={{position: 'relative'}}>
|
||||
<CaptchaKey field={field} fields={props.fields} label={labelWrap(field)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if ( field.type==='number' ){
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor} style={{ position: 'relative'}}>
|
||||
<NumberControl
|
||||
required={ field.required }
|
||||
placeholder={ field.placeholder }
|
||||
className="number_full"
|
||||
disabled={ disabled }
|
||||
help={ field.comment }
|
||||
label={labelWrap(field)}
|
||||
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
|
||||
value= { fieldValue }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (field.type==='text' ){
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor} style={{position: 'relative'}}>
|
||||
<TextControl
|
||||
required={ field.required }
|
||||
placeholder={ field.placeholder }
|
||||
disabled={ disabled }
|
||||
help={ field.comment }
|
||||
label={labelWrap(field)}
|
||||
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
|
||||
value= { fieldValue }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if ( field.type==='button' ){
|
||||
return (
|
||||
<div className={'rsssl-field-button ' + highLightClass} ref={scrollAnchor}>
|
||||
<label>{field.label}</label>
|
||||
<Button field={field}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ( field.type==='password' ){
|
||||
return (
|
||||
<div className={ highLightClass} ref={scrollAnchor}>
|
||||
<Password
|
||||
index={ props.index }
|
||||
field={ field }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ( field.type==='textarea' ) {
|
||||
// Handle csp_frame_ancestors_urls differently. Disable on select change
|
||||
let fieldDisabled = false
|
||||
if ( field.id === 'csp_frame_ancestors_urls') {
|
||||
if ( getFieldValue('csp_frame_ancestors') === 'disabled' ) {
|
||||
fieldDisabled = true
|
||||
}
|
||||
} else {
|
||||
fieldDisabled = field.disabled
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<TextareaControl
|
||||
label={ field.label }
|
||||
help={ field.comment }
|
||||
value= { fieldValue }
|
||||
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
|
||||
disabled={ fieldDisabled }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ( field.type==='license' ){
|
||||
let field = props.field;
|
||||
let fieldValue = field.value;
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<License index={props.index} field={field} fieldValue={fieldValue}/>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
if ( field.type==='number' ){
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<NumberControl
|
||||
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
|
||||
help={ field.comment }
|
||||
label={ field.label }
|
||||
value= { fieldValue }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if ( field.type==='email' ){
|
||||
return (
|
||||
<div className={this.highLightClass} ref={this.scrollAnchor}>
|
||||
<TextControl
|
||||
help={ field.comment }
|
||||
label={ field.label }
|
||||
onChange={ ( fieldValue ) => this.onChangeHandler(fieldValue) }
|
||||
value= { fieldValue }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ( field.type==='host') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<Host
|
||||
index={props.index}
|
||||
field={props.field}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if ( field.type==='select') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<SelectControl
|
||||
disabled={ disabled }
|
||||
label={labelWrap(field)}
|
||||
onChangeHandler={ ( fieldValue ) => onChangeHandler(fieldValue) }
|
||||
value= { fieldValue }
|
||||
options={ options }
|
||||
field={field}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if ( field.type==='support' ) {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<Support/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if ( field.type==='postdropdown' ) {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<PostDropdown field={props.field}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if ( field.type==='permissionspolicy' ) {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<PermissionsPolicy disabled={disabled} field={props.field} options={options}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type==='captcha') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<Captcha field={field} label={labelWrap(field)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if ( field.type==='learningmode' ) {
|
||||
return(
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<LearningMode disabled={disabled} field={props.field}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if ( field.type==='riskcomponent' ) {
|
||||
return (<div className={highLightClass} ref={scrollAnchor}>
|
||||
<RiskComponent field={props.field}/>
|
||||
</div>)
|
||||
}
|
||||
|
||||
if ( field.type === 'mixedcontentscan' ) {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<MixedContentScan field={props.field}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'vulnerabilitiestable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<VulnerabilitiesOverview field={props.field} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'two_fa_roles') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<label htmlFor={`rsssl-two-fa-dropdown-${field.id}`}>
|
||||
{labelWrap(field)}
|
||||
</label>
|
||||
<TwoFaRolesDropDown field={props.field} forcedRoledId={props.field.forced_roles_id} optionalRolesId={props.field.optional_roles_id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === 'eventlog-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<EventLogDataTable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (field.type === 'twofa-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<TwoFaDataTable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'ip-address-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<IpAddressDatatable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'user-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<UserDatatable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'file-change-detection') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<FileChangeDetection
|
||||
field={props.field}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'country-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<CountryDatatable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'geo-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<GeoDatatable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'geo-ip-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<WhiteListDatatable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'blocklist-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<BlockListDatatable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'user-agents-datatable') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<UserAgentTable
|
||||
field={props.field}
|
||||
action={props.field.action}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'roles_dropdown') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<label htmlFor={`rsssl-two-fa-dropdown-${field.id}`}>
|
||||
{labelWrap(field)}
|
||||
</label>
|
||||
<RolesDropDown field={props.field}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === 'roles_enabled_dropdown') {
|
||||
return (
|
||||
<div className={highLightClass} ref={scrollAnchor}>
|
||||
<label htmlFor={`rsssl-two-fa-dropdown-${field.id}`}>
|
||||
{labelWrap(field)}
|
||||
</label>
|
||||
<TwoFaEnabledDropDown field={props.field} disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if(field.type === 'notificationtester') {
|
||||
return (
|
||||
<div className={'rsssl-field-button ' + highLightClass} ref={scrollAnchor}>
|
||||
<NotificationTester field={props.field} disabled={disabled} labelWrap={labelWrap}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if ( field.type === 'letsencrypt' ) {
|
||||
return (
|
||||
<LetsEncrypt field={field} />
|
||||
)
|
||||
}
|
||||
|
||||
if ( field.type === 'activate' ) {
|
||||
return (
|
||||
<Activate field={field}/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
'not found field type '+field.type
|
||||
);
|
||||
}
|
||||
|
||||
export default Field;
|
||||
@@ -0,0 +1,311 @@
|
||||
import {create} from 'zustand';
|
||||
import {produce} from 'immer';
|
||||
import * as rsssl_api from "../utils/api";
|
||||
import {__} from '@wordpress/i18n';
|
||||
import {toast} from 'react-toastify';
|
||||
import {ReactConditions} from "../utils/ReactConditions";
|
||||
|
||||
const fetchFields = () => {
|
||||
return rsssl_api.getFields().then((response) => {
|
||||
let fields = response.fields;
|
||||
let progress = response.progress;
|
||||
let error = response.error;
|
||||
return {fields, progress, error};
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
const useFields = create(( set, get ) => ({
|
||||
fieldsLoaded: false,
|
||||
error:false,
|
||||
fields: [],
|
||||
changedFields:[],
|
||||
progress:[],
|
||||
nextButtonDisabled:false,
|
||||
overrideNextButtonDisabled:false,
|
||||
refreshTests:false,
|
||||
highLightField: '',
|
||||
setHighLightField: (highLightField) => {
|
||||
set({ highLightField });
|
||||
},
|
||||
|
||||
setRefreshTests: (refreshTests) => set(state => ({ refreshTests })),
|
||||
handleNextButtonDisabled: (nextButtonDisabled) => {
|
||||
set({overrideNextButtonDisabled: nextButtonDisabled});
|
||||
},
|
||||
setChangedField: (id, value) => {
|
||||
set(
|
||||
produce((state) => {
|
||||
//remove current reference
|
||||
const existingFieldIndex = state.changedFields.findIndex(field => {
|
||||
return field.id===id;
|
||||
});
|
||||
|
||||
if (existingFieldIndex!==-1){
|
||||
state.changedFields.splice(existingFieldIndex, 1);
|
||||
}
|
||||
|
||||
//add again, with new value
|
||||
let field = {};
|
||||
field.id = id;
|
||||
field.value = value;
|
||||
state.changedFields.push(field);
|
||||
})
|
||||
)
|
||||
},
|
||||
showSavedSettingsNotice : (text , type = 'success') => {
|
||||
handleShowSavedSettingsNotice(text, type);
|
||||
},
|
||||
|
||||
updateField: (id, value) => {
|
||||
set(
|
||||
produce((state) => {
|
||||
let index = state.fields.findIndex(fieldItem => fieldItem.id === id);
|
||||
if (index !== -1) {
|
||||
state.fields[index].value = value;
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
updateFieldAttribute: (id, attribute, value) => {
|
||||
set(
|
||||
produce((state) => {
|
||||
let index = state.fields.findIndex(fieldItem => fieldItem.id === id);
|
||||
if (index !== -1) {
|
||||
state.fields[index][attribute] = value;
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
updateSubField: (id, subItemId, value) => {
|
||||
set(
|
||||
produce((state) => {
|
||||
let index = state.fields.findIndex(fieldItem => fieldItem.id === id);
|
||||
let itemValue = state.fields[index].value;
|
||||
if (!Array.isArray(itemValue)) {
|
||||
itemValue = [];
|
||||
}
|
||||
|
||||
let subIndex = itemValue.findIndex(subItem => subItem.id === subItemId);
|
||||
if (subIndex !== -1) {
|
||||
state.fields[index].updateItemId = subItemId;
|
||||
state.fields[index].value[subIndex]['value'] = value;
|
||||
state.fields[index].value = itemValue.map(item => {
|
||||
const { deleteControl, valueControl, statusControl, ...rest } = item;
|
||||
return rest;
|
||||
});
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
removeHelpNotice: (id) => {
|
||||
set(
|
||||
produce((state) => {
|
||||
const fieldIndex = state.fields.findIndex(field => {
|
||||
return field.id===id;
|
||||
});
|
||||
state.fields[fieldIndex].help = false;
|
||||
})
|
||||
)
|
||||
},
|
||||
addHelpNotice : (id, label, text, title, url) => {
|
||||
get().removeHelpNotice(id);
|
||||
//create help object
|
||||
|
||||
let help = {};
|
||||
help.label=label;
|
||||
help.text=text;
|
||||
if (url) help.url=url;
|
||||
if (title) help.title=title;
|
||||
set(
|
||||
produce((state) => {
|
||||
const fieldIndex = state.fields.findIndex(field => {
|
||||
return field.id===id;
|
||||
});
|
||||
if (fieldIndex!==-1) {
|
||||
state.fields[fieldIndex].help = help;
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
fieldAlreadyEnabled: (id) => {
|
||||
let fieldIsChanged = get().changedFields.filter(field => field.id === id ).length>0;
|
||||
let fieldIsEnabled = get().getFieldValue(id);
|
||||
return !fieldIsChanged && fieldIsEnabled;
|
||||
},
|
||||
getFieldValue : (id) => {
|
||||
let fields = get().fields;
|
||||
let fieldItem = fields.filter(field => field.id === id )[0];
|
||||
if (fieldItem){
|
||||
return fieldItem.value;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
getField : (id) => {
|
||||
let fields = get().fields;
|
||||
let fieldItem = fields.filter(field => field.id === id )[0];
|
||||
if (fieldItem){
|
||||
return fieldItem;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
saveFields: async (skipRefreshTests, showSavedNotice, force = false) => {
|
||||
let refreshTests = typeof skipRefreshTests !== 'undefined' ? skipRefreshTests : true;
|
||||
showSavedNotice = typeof showSavedNotice !== 'undefined' ? showSavedNotice : true;
|
||||
let fields = get().fields;
|
||||
fields = fields.filter(field => field.data_target !== 'banner');
|
||||
let changedFields = get().changedFields;
|
||||
let saveFields = [];
|
||||
//data_target
|
||||
for (const field of fields) {
|
||||
let fieldIsIncluded = changedFields.filter(changedField => changedField.id === field.id).length > 0;
|
||||
//also check if there's no saved value yet for radio fields, by checking the never_saved attribute.
|
||||
//a radio or select field looks like it's completed, but won't save if it isn't changed.
|
||||
//this should not be the case for disabled fields, as these fields often are enabled server side because they're enabled outside Really Simple Security.
|
||||
let select_or_radio = field.type === 'select' || field.type === 'radio';
|
||||
if (fieldIsIncluded || (field.never_saved && !field.disabled && select_or_radio)) {
|
||||
saveFields.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
//if no fields were changed, do nothing.
|
||||
if (saveFields.length > 0 || force === true) {
|
||||
let response = rsssl_api.setFields(saveFields).then((response) => {
|
||||
return response;
|
||||
})
|
||||
|
||||
if (showSavedNotice) {
|
||||
toast.promise(
|
||||
response,
|
||||
{
|
||||
pending: __('Saving settings...', 'really-simple-ssl'),
|
||||
success: __('Settings saved', 'really-simple-ssl'),
|
||||
error: __('Something went wrong', 'really-simple-ssl'),
|
||||
}
|
||||
);
|
||||
}
|
||||
await response.then((response) => {
|
||||
set(
|
||||
produce((state) => {
|
||||
state.changedFields = [];
|
||||
state.fields = response.fields;
|
||||
state.progress = response.progress;
|
||||
state.refreshTests = refreshTests;
|
||||
})
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
if (showSavedNotice && saveFields.length === 0) {
|
||||
//nothing to save. show instant success.
|
||||
toast.promise(
|
||||
Promise.resolve(),
|
||||
{
|
||||
success: __('Settings saved', 'really-simple-ssl'),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
updateFieldsData: (selectedSubMenuItem) => {
|
||||
let fields = get().fields;
|
||||
fields = updateFieldsListWithConditions(fields);
|
||||
|
||||
//only if selectedSubMenuItem is actually passed
|
||||
if (selectedSubMenuItem) {
|
||||
let nextButtonDisabled = isNextButtonDisabled(fields, selectedSubMenuItem);
|
||||
//if the button was set to disabled with the handleNextButtonDisabled function, we give that priority until it's released.
|
||||
if (get().overrideNextButtonDisabled) {
|
||||
nextButtonDisabled = get().overrideNextButtonDisabled;
|
||||
}
|
||||
set(
|
||||
produce((state) => {
|
||||
state.nextButtonDisabled = nextButtonDisabled;
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
set(
|
||||
produce((state) => {
|
||||
state.fields = fields;
|
||||
})
|
||||
)
|
||||
},
|
||||
fetchFieldsData: async ( selectedSubMenuItem ) => {
|
||||
const { fields, progress, error } = await fetchFields();
|
||||
let conditionallyEnabledFields = updateFieldsListWithConditions(fields);
|
||||
let selectedFields = conditionallyEnabledFields.filter(field => field.menu_id === selectedSubMenuItem);
|
||||
set({fieldsLoaded: true, fields:conditionallyEnabledFields, selectedFields:selectedFields, progress:progress, error: error });
|
||||
}
|
||||
}));
|
||||
|
||||
export default useFields;
|
||||
|
||||
//check if all required fields have been enabled. If so, enable save/continue button
|
||||
const isNextButtonDisabled = (fields, selectedMenuItem) => {
|
||||
let fieldsOnPage = [];
|
||||
//get all fields with group_id this.props.group_id
|
||||
for (const field of fields){
|
||||
if (field.menu_id === selectedMenuItem ){
|
||||
fieldsOnPage.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
let requiredFields = fieldsOnPage.filter(field => field.required && !field.conditionallyDisabled && (field.value.length==0 || !field.value) );
|
||||
return requiredFields.length > 0;
|
||||
}
|
||||
|
||||
const updateFieldsListWithConditions = (fields) => {
|
||||
let newFields = [];
|
||||
if (!fields || !Array.isArray(fields)) {
|
||||
return [];
|
||||
}
|
||||
fields.forEach(function(field, i) {
|
||||
// Create instance to evaluate the react_conditions of a field
|
||||
let reactConditions = new ReactConditions(field, fields);
|
||||
let fieldCurrentlyEnabled = reactConditions.isEnabled();
|
||||
|
||||
//we want to update the changed fields if this field has just become visible. Otherwise the new field won't get saved.
|
||||
const newField = {...field};
|
||||
newField.conditionallyDisabled = (fieldCurrentlyEnabled === false);
|
||||
|
||||
// ReactConditions will keep the original disabledTooltipText if the
|
||||
// field is not disabled by the react_conditions. Key differs from the
|
||||
// one in the config on purpose to prevent overwriting the config value.
|
||||
newField.disabledTooltipHoverText = reactConditions.getDisabledTooltipText();
|
||||
|
||||
// If the field is a letsencrypt field or has a condition_action of
|
||||
// 'hide', it should be hidden if the field is disabled.
|
||||
newField.visible = !(!fieldCurrentlyEnabled && (newField.type === 'letsencrypt' || newField.condition_action === 'hide'));
|
||||
newFields.push(newField);
|
||||
});
|
||||
return newFields;
|
||||
}
|
||||
|
||||
const handleShowSavedSettingsNotice = ( text, type ) => {
|
||||
if (typeof text === 'undefined') {
|
||||
text = __( 'Settings saved', 'really-simple-ssl' );
|
||||
}
|
||||
|
||||
if (typeof type === 'undefined') {
|
||||
type = 'success';
|
||||
}
|
||||
|
||||
if (type === 'error') {
|
||||
toast.error(text);
|
||||
}
|
||||
|
||||
if (type === 'warning') {
|
||||
toast.warning(text);
|
||||
}
|
||||
|
||||
if (type === 'info') {
|
||||
toast.info(text);
|
||||
}
|
||||
|
||||
if (type === 'success') {
|
||||
toast.success(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
import DataTableWrapper from "../DataTable/DataTableWrapper";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import DataTableStore from "../DataTable/DataTableStore";
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import useFields from "../FieldsData";
|
||||
import useMenu from "../../Menu/MenuData";
|
||||
import {toast} from "react-toastify";
|
||||
|
||||
const FileChangeDetection = ({field}) => {
|
||||
const {
|
||||
clearAllData,
|
||||
setProcessing,
|
||||
} = DataTableStore();
|
||||
const { updateFieldsData, showSavedSettingsNotice } = useFields();
|
||||
const { selectedSubMenuItem} = useMenu();
|
||||
const enabled = true;
|
||||
const handleClick = async () => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
'reset_changed_files',
|
||||
{}
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
showSavedSettingsNotice(__('File changes have been been reset', 'really-simple-ssl') );
|
||||
clearAllData();
|
||||
setProcessing(false);
|
||||
//field now should be disabled, as it's now processing
|
||||
updateFieldsData(selectedSubMenuItem);
|
||||
}
|
||||
}
|
||||
|
||||
let controlButton = {
|
||||
show:true,
|
||||
onClick:handleClick,
|
||||
label:__("Reset changed files", "really-simple-ssl")
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTableWrapper
|
||||
field={field}
|
||||
controlButton={controlButton}
|
||||
enabled={true}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileChangeDetection;
|
||||
@@ -0,0 +1,17 @@
|
||||
// FilterData.js
|
||||
import {create} from 'zustand';
|
||||
|
||||
const filterData = create((set, get) => ({
|
||||
selectedFilter: [],
|
||||
processingFilter: false,
|
||||
setSelectedFilter: (selectedFilter, activeGroupId) => {
|
||||
set((state) => ({
|
||||
//we make it an array, so we can have multiple filters
|
||||
selectedFilter: {...state.selectedFilter, [activeGroupId]: selectedFilter},
|
||||
}));
|
||||
},
|
||||
getCurrentFilter: (activeGroupId) => get().selectedFilter[activeGroupId],
|
||||
setProcessingFilter: (processingFilter) => set({processingFilter}),
|
||||
}));
|
||||
|
||||
export default filterData;
|
||||
@@ -0,0 +1,19 @@
|
||||
import Icon from "../../utils/Icon";
|
||||
|
||||
const AddButton = ({ getCurrentFilter, moduleName, handleOpen, processing, blockedText, allowedText, disabled }) => {
|
||||
return (
|
||||
<div className="rsssl-add-button">
|
||||
<div className="rsssl-add-button__inner">
|
||||
<button
|
||||
className="button button-secondary button-datatable rsssl-add-button__button"
|
||||
onClick={handleOpen}
|
||||
disabled={disabled}
|
||||
>
|
||||
{allowedText}{processing && <Icon name = "loading" color = 'grey' />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddButton;
|
||||
@@ -0,0 +1,370 @@
|
||||
import React, { useEffect, useState, useCallback } from '@wordpress/element';
|
||||
import DataTable, {createTheme} from "react-data-table-component";
|
||||
import FieldsData from "../FieldsData";
|
||||
import WhiteListTableStore from "./WhiteListTableStore";
|
||||
import FilterData from "../FilterData";
|
||||
import Flag from "../../utils/Flag/Flag";
|
||||
import {__} from '@wordpress/i18n';
|
||||
import useFields from "../FieldsData";
|
||||
import AddButton from "./AddButton";
|
||||
import TrustIpAddressModal from "./TrustIpAddressModal";
|
||||
|
||||
const BlockListDatatable = (props) => {
|
||||
const {
|
||||
BlockListData,
|
||||
WhiteListTable,
|
||||
setDataLoaded,
|
||||
dataLoaded,
|
||||
dataLoaded_block,
|
||||
fetchData,
|
||||
processing_block,
|
||||
ipAddress,
|
||||
pagination,
|
||||
resetRow,
|
||||
rowCleared,
|
||||
} = WhiteListTableStore();
|
||||
|
||||
const {showSavedSettingsNotice, saveFields} = FieldsData();
|
||||
|
||||
const [rowsSelected, setRowsSelected] = useState([]);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [tableHeight, setTableHeight] = useState(600); // Starting height
|
||||
const rowHeight = 50; // Height of each row.
|
||||
const moduleName = 'rsssl-group-filter-firewall_block_list_listing';
|
||||
const {fields, fieldAlreadyEnabled, getFieldValue} = useFields();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [DataTable, setDataTable] = useState(null);
|
||||
const [theme, setTheme] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
import('react-data-table-component').then((module) => {
|
||||
const { default: DataTable, createTheme } = module;
|
||||
setDataTable(() => DataTable);
|
||||
setTheme(() => createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light'));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
const handlePerRowsChange = (newRowsPerPage) => {
|
||||
setRowsPerPage(newRowsPerPage);
|
||||
};
|
||||
|
||||
const {
|
||||
getCurrentFilter,
|
||||
} = FilterData();
|
||||
|
||||
const [filter, setFilter] = useState(getCurrentFilter(moduleName));
|
||||
|
||||
/**
|
||||
* Build a column configuration object.
|
||||
*
|
||||
* @param {object} column - The column object.
|
||||
* @param {string} column.name - The name of the column.
|
||||
* @param {boolean} column.sortable - Whether the column is sortable.
|
||||
* @param {boolean} column.searchable - Whether the column is searchable.
|
||||
* @param {number} column.width - The width of the column.
|
||||
* @param {boolean} column.visible - Whether the column is visible.
|
||||
* @param {string} column.column - The column identifier.
|
||||
*
|
||||
* @returns {object} The column configuration object.
|
||||
*/
|
||||
const buildColumn = useCallback((column) => ({
|
||||
//if the filter is set to region and the columns = status we do not want to show the column
|
||||
name: column.name,
|
||||
sortable: column.sortable,
|
||||
searchable: column.searchable,
|
||||
width: column.width,
|
||||
visible: column.visible,
|
||||
column: column.column,
|
||||
selector: row => row[column.column],
|
||||
}), [filter]);
|
||||
let field = props.field;
|
||||
const columns = field.columns.map(buildColumn);
|
||||
|
||||
const searchableColumns = columns
|
||||
.filter(column => column.searchable)
|
||||
.map(column => column.column);
|
||||
|
||||
useEffect(() => {
|
||||
setRowsSelected([]);
|
||||
}, [BlockListData]);
|
||||
|
||||
let enabled = getFieldValue('enable_firewall');
|
||||
|
||||
useEffect(() => {
|
||||
const currentFilter = getCurrentFilter(moduleName);
|
||||
if (typeof currentFilter === 'undefined') {
|
||||
setFilter('all');
|
||||
} else {
|
||||
setFilter(currentFilter);
|
||||
}
|
||||
setRowsSelected([]);
|
||||
// resetRowSelection(true);
|
||||
// resetRowSelection(false);
|
||||
}, [getCurrentFilter(moduleName)]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
saveFields(false, false)
|
||||
};
|
||||
}, [enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof filter !== 'undefined') {
|
||||
fetchData(field.action, filter);
|
||||
}
|
||||
}, [filter]);
|
||||
|
||||
useEffect(() => {
|
||||
if(!dataLoaded_block) {
|
||||
fetchData(field.action, filter);
|
||||
}
|
||||
}, [dataLoaded_block]);
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light');
|
||||
|
||||
const handleSelection = useCallback((state) => {
|
||||
//based on the current page and the rows per page we get the rows that are selected
|
||||
const {selectedCount, selectedRows, allSelected, allRowsSelected} = state;
|
||||
let rows = [];
|
||||
if (allSelected) {
|
||||
rows = selectedRows.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage);
|
||||
setRowsSelected(rows);
|
||||
} else {
|
||||
setRowsSelected(selectedRows);
|
||||
}
|
||||
}, [currentPage, rowsPerPage]);
|
||||
|
||||
const allowById = useCallback((id) => {
|
||||
//We check if the id is an array
|
||||
if (Array.isArray(id)) {
|
||||
//We get all the iso2 codes and names from the array
|
||||
const ids = id.map(item => ({
|
||||
id: item.id,
|
||||
}));
|
||||
//we loop through the ids and allow them one by one
|
||||
ids.forEach((id) => {
|
||||
resetRow(id.id).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
});
|
||||
// fetchData(field.action, filter ? filter : 'all');
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
resetRow(id).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
// fetchData(field.action, filter ? filter : 'all');
|
||||
}
|
||||
|
||||
setDataLoaded(false);
|
||||
|
||||
}, [resetRow]);
|
||||
const data = {...BlockListData.data};
|
||||
|
||||
const generateFlag = useCallback((flag, title) => (
|
||||
<>
|
||||
<Flag
|
||||
countryCode={flag}
|
||||
style={{
|
||||
fontSize: '2em',
|
||||
}}
|
||||
title={title}
|
||||
/>
|
||||
</>
|
||||
), []);
|
||||
|
||||
const ActionButton = ({ onClick, children, className }) => (
|
||||
// <div className={`rsssl-action-buttons__inner`}>
|
||||
<button
|
||||
className={`button ${className} rsssl-action-buttons__button`}
|
||||
onClick={onClick}
|
||||
disabled={processing_block}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
// </div>
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
setModalOpen(false);
|
||||
}
|
||||
|
||||
const handleOpen = () => {
|
||||
setModalOpen(true);
|
||||
}
|
||||
|
||||
const generateActionButtons = useCallback((id) => {
|
||||
return (<div className="rsssl-action-buttons">
|
||||
<ActionButton
|
||||
onClick={() => allowById(id)} className="button-red">
|
||||
{__("Reset", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</div>)
|
||||
}, [moduleName, allowById]);
|
||||
|
||||
|
||||
|
||||
for (const key in data) {
|
||||
const dataItem = {...data[key]};
|
||||
dataItem.action = generateActionButtons(dataItem.id);
|
||||
dataItem.flag = generateFlag(dataItem.iso2_code, dataItem.country_name);
|
||||
dataItem.status = __(dataItem.status = dataItem.status.charAt(0).toUpperCase() + dataItem.status.slice(1), 'really-simple-ssl');
|
||||
data[key] = dataItem;
|
||||
}
|
||||
|
||||
let paginationSet;
|
||||
paginationSet = typeof pagination !== 'undefined';
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(data).length === 0 ) {
|
||||
setTableHeight(100); // Adjust depending on your UI measurements
|
||||
} else {
|
||||
setTableHeight(rowHeight * (paginationSet ? pagination.perPage + 2 : 12)); // Adjust depending on your UI measurements
|
||||
}
|
||||
|
||||
}, [paginationSet, pagination?.perPage, data]);
|
||||
|
||||
useEffect(() => {
|
||||
let intervals = [];
|
||||
const filteredData = Object.entries(data)
|
||||
.filter(([_, dataItem]) => {
|
||||
return Object.values(dataItem).some(val => ((val ?? '').toString().toLowerCase().includes(searchTerm.toLowerCase())));
|
||||
})
|
||||
.map(([key, dataItem]) => {
|
||||
const newItem = { ...dataItem };
|
||||
newItem.action = generateActionButtons(newItem.id);
|
||||
newItem.flag = generateFlag(newItem.iso2_code, newItem.country_name);
|
||||
newItem.status = __(newItem.status = newItem.status.charAt(0).toUpperCase() + newItem.status.slice(1), 'really-simple-ssl');
|
||||
// if the newItem.time_left not is 0 we count down in seconds the value
|
||||
if (newItem.time_left > 0) {
|
||||
const interval = setInterval(() => {
|
||||
newItem.time_left--;
|
||||
}, 1000);
|
||||
intervals.push(interval);
|
||||
}
|
||||
return [key, newItem];
|
||||
})
|
||||
.reduce((obj, [key, val]) => ({ ...obj, [key]: val }), {});
|
||||
}, [searchTerm, data]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<TrustIpAddressModal
|
||||
isOpen={modalOpen}
|
||||
onRequestClose={handleClose}
|
||||
value={ipAddress}
|
||||
status={'blocked'}
|
||||
filter={filter? filter : 'all'}
|
||||
>
|
||||
</TrustIpAddressModal>
|
||||
<div className="rsssl-container">
|
||||
{/*display the add button on left side*/}
|
||||
<AddButton
|
||||
moduleName={moduleName}
|
||||
handleOpen={handleOpen}
|
||||
processing={processing_block}
|
||||
allowedText={__("Block IP Address", "really-simple-ssl")}
|
||||
/>
|
||||
<div className="rsssl-search-bar">
|
||||
<div className="rsssl-search-bar__inner">
|
||||
<div className="rsssl-search-bar__icon"></div>
|
||||
<input
|
||||
type="text"
|
||||
className="rsssl-search-bar__input"
|
||||
placeholder={__("Search", "really-simple-ssl")}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{rowsSelected.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
}}
|
||||
>
|
||||
<div className={"rsssl-multiselect-datatable-form rsssl-primary"}>
|
||||
<div>
|
||||
{__("You have selected %s rows", "really-simple-ssl").replace('%s', rowsSelected.length)}
|
||||
</div>
|
||||
<div className="rsssl-action-buttons">
|
||||
<>
|
||||
<ActionButton
|
||||
onClick={() => allowById(rowsSelected)} className="button-red">
|
||||
{__("Reset", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{DataTable &&
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={Object.values(data).filter((row) => {
|
||||
return Object.values(row).some((val) => ((val ?? '').toString().toLowerCase()).includes(searchTerm.toLowerCase()));
|
||||
})}
|
||||
dense
|
||||
pagination={true}
|
||||
paginationComponentOptions={{
|
||||
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
|
||||
rangeSeparatorText: __('of', 'really-simple-ssl'),
|
||||
noRowsPerPage: false,
|
||||
selectAllRowsItem: false,
|
||||
selectAllRowsItemText: __('All', 'really-simple-ssl'),
|
||||
|
||||
}}
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
selectableRows={true}
|
||||
clearSelectedRows={rowCleared}
|
||||
paginationPerPage={rowsPerPage}
|
||||
onChangePage={handlePageChange}
|
||||
onChangeRowsPerPage={handlePerRowsChange}
|
||||
onSelectedRowsChange={handleSelection}
|
||||
theme="really-simple-plugins"
|
||||
customStyles={customStyles}
|
||||
/>}
|
||||
{!enabled && (
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay"><span
|
||||
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Here you can add IP addresses that should never be blocked by region restrictions.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlockListDatatable;
|
||||
@@ -0,0 +1,316 @@
|
||||
/* Creates A Store For Risk Data using Zustand */
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import {produce} from "immer";
|
||||
import React from "react";
|
||||
import GeoDatatable from "./GeoDatatable";
|
||||
|
||||
const GeoDataTableStore = create((set, get) => ({
|
||||
|
||||
processing: false,
|
||||
dataLoaded: false,
|
||||
pagination: {},
|
||||
dataActions: {},
|
||||
CountryDataTable: [],
|
||||
rowCleared: false,
|
||||
|
||||
fetchCountryData: async (action, filterValue) => {
|
||||
//we check if the processing is already true, if so we return
|
||||
set({
|
||||
processing: true,
|
||||
rowCleared: true,
|
||||
dataLoaded: false
|
||||
});
|
||||
// if the filterValue is not set, we do nothing.
|
||||
if (!filterValue) {
|
||||
set({processing: false});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action, {filterValue}
|
||||
);
|
||||
//now we set the EventLog
|
||||
if (response && response.request_success) {
|
||||
set({
|
||||
CountryDataTable: response,
|
||||
dataLoaded: true,
|
||||
processing: false
|
||||
});
|
||||
}
|
||||
set({ rowCleared: true });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
|
||||
handleCountryTableSearch: async (search, searchColumns) => {
|
||||
//Add the search to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, search, searchColumns};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
handleCountryTablePageChange: async (page, pageSize) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, page, pageSize};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
handleCountryTableRowsChange: async (currentRowsPerPage, currentPage) => {
|
||||
//Add the page and pageSize to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, currentRowsPerPage, currentPage};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
//this handles all pagination and sorting
|
||||
handleCountryTableSort: async (column, sortDirection) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, sortColumn: column, sortDirection};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
handleCountryTableFilter: async (column, filterValue) => {
|
||||
//Add the column and sortDirection to the dataActions
|
||||
set(produce((state) => {
|
||||
state.dataActions = {...state.dataActions, filterColumn: column, filterValue};
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
/*
|
||||
* This function add a new row to the table
|
||||
*/
|
||||
addRow: async (country, name) => {
|
||||
set({rowCleared: false});
|
||||
let data = {
|
||||
country_code: country,
|
||||
country_name: name
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_add_blocked_country', data);
|
||||
if (response && response.request_success) {
|
||||
set({rowCleared: true});
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Return a custom error message or the API response message.
|
||||
return { success: false, message: response?.message || 'Failed to add country', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Return the caught error with a custom message.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
}
|
||||
},
|
||||
|
||||
addMultiRow: async (countries) => {
|
||||
set({processing: true});
|
||||
set({rowCleared: false});
|
||||
let data = {
|
||||
country_codes: countries
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_add_blocked_country', data);
|
||||
if (response && response.request_success) {
|
||||
set({rowCleared: true});
|
||||
// Return the success message from the API response.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
set({rowCleared: true});
|
||||
// Return a custom error message or the API response message.
|
||||
return { success: false, message: response?.message || 'Failed to add countries', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
set({rowCleared: true});
|
||||
// Return the caught error with a custom message.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
set({rowCleared: true});
|
||||
}
|
||||
},
|
||||
|
||||
removeRowMulti: async (countries, dataActions) => {
|
||||
set({processing: true});
|
||||
set({rowCleared: false});
|
||||
let data = {
|
||||
country_codes: countries
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_remove_blocked_country', data);
|
||||
if (response && response.request_success) {
|
||||
set({rowCleared: true});
|
||||
// Return the success message from the API response.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Return a custom error message or the API response message.
|
||||
return { success: false, message: response?.message || 'Failed to remove countries', response };
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
set({rowCleared: true});
|
||||
// Return the caught error with a custom message.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({rowCleared: true});
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
|
||||
removeRow: async (country) => {
|
||||
set({processing: true});
|
||||
set({rowCleared: false});
|
||||
let data = {
|
||||
country_code: country
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_remove_blocked_country', data);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
set({rowCleared: true});
|
||||
// Potentially notify the user of success, if needed.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Handle any unsuccessful response if needed.
|
||||
set({rowCleared: true});
|
||||
return { success: false, message: response?.message || 'Failed to remove country', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Notify the user of an error.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({rowCleared: true});
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
|
||||
addRegion: async (region) => {
|
||||
set({processing: true});
|
||||
set({rowCleared: false});
|
||||
let data = {
|
||||
region_code: region
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_add_blocked_region', data);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
set({rowCleared: true});
|
||||
// Potentially notify the user of success, if needed.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Handle any unsuccessful response if needed.
|
||||
set({rowCleared: true});
|
||||
return { success: false, message: response?.message || 'Failed to add region', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Notify the user of an error.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
set({rowCleared: true});
|
||||
}
|
||||
},
|
||||
|
||||
addRegionsMulti: async (regions, dataActions) => {
|
||||
set({processing: true});
|
||||
set({rowCleared: false});
|
||||
let data = {
|
||||
region_codes: regions
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_add_blocked_region', data);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
set({rowCleared: true});
|
||||
// Potentially notify the user of success, if needed.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
set({rowCleared: true});
|
||||
// Handle any unsuccessful response if needed.
|
||||
return { success: false, message: response?.message || 'Failed to add regions', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Notify the user of an error.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({rowCleared: true});
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
|
||||
removeRegion: async (region) => {
|
||||
set({processing: true});
|
||||
set({rowCleared: false});
|
||||
let data = {
|
||||
region_code: region
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_remove_blocked_region', data);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
set({rowCleared: true});
|
||||
// Potentially notify the user of success, if needed.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Handle any unsuccessful response if needed.
|
||||
set({rowCleared: true});
|
||||
return { success: false, message: response?.message || 'Failed to remove region', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Notify the user of an error.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
set({rowCleared: true});
|
||||
}
|
||||
},
|
||||
|
||||
removeRegionMulti: async (regions) => {
|
||||
set({processing: true});
|
||||
set({rowCleared: false});
|
||||
let data = {
|
||||
region_codes: regions
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_remove_blocked_region', data);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
// Potentially notify the user of success, if needed.
|
||||
set({rowCleared: true});
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
set({rowCleared: true});
|
||||
// Handle any unsuccessful response if needed.
|
||||
return { success: false, message: response?.message || 'Failed to remove regions', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Notify the user of an error.
|
||||
set({rowCleared: true});
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
set({rowCleared: true});
|
||||
}
|
||||
},
|
||||
|
||||
resetRowSelection: async (on_off) => {
|
||||
set({rowCleared: on_off});
|
||||
}
|
||||
}));
|
||||
|
||||
export default GeoDataTableStore;
|
||||
@@ -0,0 +1,528 @@
|
||||
import {useEffect, useState, useCallback} from '@wordpress/element';
|
||||
import FieldsData from "../FieldsData";
|
||||
import GeoDataTableStore from "./GeoDataTableStore";
|
||||
import EventLogDataTableStore from "../EventLog/EventLogDataTableStore";
|
||||
import FilterData from "../FilterData";
|
||||
import Flag from "../../utils/Flag/Flag";
|
||||
import {__} from '@wordpress/i18n';
|
||||
import useFields from "../FieldsData";
|
||||
import useMenu from "../../Menu/MenuData";
|
||||
|
||||
/**
|
||||
* A component for displaying a geo datatable.
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string} props.field - The field to display.
|
||||
*
|
||||
* @returns {JSX.Element} The rendered component.
|
||||
*/
|
||||
const GeoDatatable = (props) => {
|
||||
const {
|
||||
CountryDataTable,
|
||||
dataLoaded,
|
||||
fetchCountryData,
|
||||
addRow,
|
||||
addMultiRow,
|
||||
removeRegion,
|
||||
removeRegionMulti,
|
||||
addRegion,
|
||||
addRegionsMulti,
|
||||
removeRow,
|
||||
removeRowMulti,
|
||||
rowCleared,
|
||||
resetRowSelection,
|
||||
} = GeoDataTableStore();
|
||||
|
||||
const moduleName = 'rsssl-group-filter-firewall_list_listing';
|
||||
const [localData, setLocalData] = useState([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [visualData, setVisualData] = useState([]);
|
||||
const {showSavedSettingsNotice, saveFields} = FieldsData();
|
||||
const [rowsSelected, setRowsSelected] = useState([]);
|
||||
const [columns, setColumns] = useState([]);
|
||||
const {fields, fieldAlreadyEnabled, getFieldValue, setHighLightField, getField} = useFields();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const {setSelectedSubMenuItem} = useMenu();
|
||||
const [DataTable, setDataTable] = useState(null);
|
||||
const [theme, setTheme] = useState(null);
|
||||
|
||||
useEffect( () => {
|
||||
import('react-data-table-component').then(({ default: DataTable, createTheme }) => {
|
||||
setDataTable(() => DataTable);
|
||||
setTheme(() => createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light'));
|
||||
});
|
||||
|
||||
}, []);
|
||||
|
||||
let enabled = getFieldValue('enable_firewall');
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
const handlePerRowsChange = (newRowsPerPage) => {
|
||||
setRowsPerPage(newRowsPerPage);
|
||||
};
|
||||
|
||||
const {
|
||||
selectedFilter,
|
||||
setSelectedFilter,
|
||||
activeGroupId,
|
||||
getCurrentFilter,
|
||||
setProcessingFilter,
|
||||
} = FilterData();
|
||||
|
||||
const [filter, setFilter] = useState(getCurrentFilter(moduleName));
|
||||
|
||||
const buildColumn = useCallback((column) => ({
|
||||
//if the filter is set to region and the columns = status we do not want to show the column
|
||||
omit: filter === 'regions' && (column.column === 'country_name' || column.column === 'flag'),
|
||||
name: (column.column === 'action' && 'regions' === filter) ? __('Block / Allow All', 'really-simple-ssl') : column.name,
|
||||
sortable: column.sortable,
|
||||
searchable: column.searchable,
|
||||
width: column.width,
|
||||
visible: column.visible,
|
||||
column: column.column,
|
||||
selector: row => row[column.column],
|
||||
}), [filter]);
|
||||
let field = props.field;
|
||||
|
||||
useEffect(() => {
|
||||
const element = document.getElementById('set_to_captcha_configuration');
|
||||
const clickListener = async event => {
|
||||
event.preventDefault();
|
||||
if (element) {
|
||||
await redirectToAddCaptcha(element);
|
||||
}
|
||||
};
|
||||
|
||||
if (element) {
|
||||
element.addEventListener('click', clickListener);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (element) {
|
||||
element.removeEventListener('click', clickListener);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const redirectToAddCaptcha = async (element) => {
|
||||
// We fetch the props from the menu item
|
||||
let menuItem = getField('enabled_captcha_provider');
|
||||
|
||||
// Create a new object based on the menuItem, including the new property
|
||||
let highlightingMenuItem = {
|
||||
...menuItem,
|
||||
highlight_field_id: 'enabled_captcha_provider',
|
||||
};
|
||||
|
||||
setHighLightField(highlightingMenuItem.highlight_field_id);
|
||||
let highlightField = getField(highlightingMenuItem.highlight_field_id);
|
||||
await setSelectedSubMenuItem(highlightField.menu_id);
|
||||
}
|
||||
|
||||
const blockCountryByCode = useCallback(async (code, name) => {
|
||||
if (Array.isArray(code)) {
|
||||
//We get all the iso2 codes and names from the array
|
||||
const ids = code.map(item => ({
|
||||
country_code: item.iso2_code,
|
||||
country_name: item.country_name
|
||||
}));
|
||||
//we loop through the ids and block them one by one
|
||||
await addMultiRow(ids).then(
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
}
|
||||
);
|
||||
await fetchCountryData(field.action, filter);
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
await addRow(code, name).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
if (result.success) {
|
||||
fetchCountryData(field.action, filter);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [addRow, filter, localData, enabled]);
|
||||
|
||||
const allowRegionByCode = useCallback(async (code, regionName = '') => {
|
||||
if (Array.isArray(code)) {
|
||||
const ids = code.map(item => ({
|
||||
iso2_code: item.iso2_code,
|
||||
country_name: item.country_name
|
||||
}));
|
||||
await removeRegionMulti(ids).then(
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
if (response.success) {
|
||||
fetchCountryData(field.action, filter);
|
||||
}
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
}
|
||||
);
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
await removeRegion(code).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
if (result.success) {
|
||||
fetchCountryData(field.action, filter);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [removeRegion, filter]);
|
||||
|
||||
const blockRegionByCode = useCallback(async (code, region = '') => {
|
||||
if (Array.isArray(code)) {
|
||||
const ids = code.map(item => ({
|
||||
iso2_code: item.iso2_code,
|
||||
country_name: item.country_name
|
||||
}));
|
||||
await addRegionsMulti(ids).then(
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
}
|
||||
);
|
||||
await fetchCountryData(field.action, filter);
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
await addRegion(code).then((result) => {
|
||||
if (result.success) {
|
||||
showSavedSettingsNotice(result.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(result.message, 'error');
|
||||
}
|
||||
});
|
||||
await fetchCountryData(field.action, filter);
|
||||
}
|
||||
|
||||
}, [addRegion, filter]);
|
||||
|
||||
const allowCountryByCode = useCallback(async (code) => {
|
||||
if (Array.isArray(code)) {
|
||||
const ids = code.map(item => ({
|
||||
country_code: item.iso2_code,
|
||||
country_name: item.country_name
|
||||
}));
|
||||
//we loop through the ids and allow them one by one
|
||||
await removeRowMulti(ids).then(
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
setRowsSelected([]);
|
||||
await fetchCountryData(field.action, filter);
|
||||
} else {
|
||||
await removeRow(code).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
await fetchCountryData(field.action, filter);
|
||||
}
|
||||
|
||||
}, [removeRow, filter]);
|
||||
|
||||
const ActionButton = ({onClick, children, className, disabled = false}) => (
|
||||
// <div className={`rsssl-action-buttons__inner`}>
|
||||
<button
|
||||
className={`button ${className} rsssl-action-buttons__button`}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
// </div>
|
||||
);
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const generateActionButtons = useCallback((code, name, region_name, showBlockButton = true, showAllowButton = true) => {
|
||||
return (<div className="rsssl-action-buttons">
|
||||
{filter === 'blocked' && (
|
||||
<ActionButton
|
||||
onClick={() => allowCountryByCode(code)}
|
||||
className="button-secondary">
|
||||
{__("Allow", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
)}
|
||||
{filter === 'regions' && (
|
||||
<>
|
||||
<ActionButton
|
||||
onClick={() => blockRegionByCode(code, region_name)}
|
||||
className="button-primary"
|
||||
disabled={!showBlockButton}
|
||||
>
|
||||
{__("Block", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={() => allowRegionByCode(code, region_name)}
|
||||
className="button-secondary"
|
||||
disabled={!showAllowButton}
|
||||
>
|
||||
{__("Allow", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</>
|
||||
)}
|
||||
{filter === 'countries' && (
|
||||
<ActionButton
|
||||
onClick={() => blockCountryByCode(code, name)}
|
||||
className="button-primary">
|
||||
{__("Block", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>)
|
||||
}, [filter]);
|
||||
|
||||
const generateFlag = useCallback((flag, title) => {
|
||||
return (
|
||||
<>
|
||||
<Flag
|
||||
countryCode={flag}
|
||||
style={{
|
||||
fontSize: '2em',
|
||||
}}
|
||||
title={title}
|
||||
continent={(getCurrentFilter(moduleName) === 'regions')}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}, [filter]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const currentFilter = getCurrentFilter(moduleName);
|
||||
if (typeof currentFilter === 'undefined') {
|
||||
setFilter('regions');
|
||||
setSelectedFilter('regions', moduleName);
|
||||
} else {
|
||||
setFilter(currentFilter);
|
||||
}
|
||||
setRowsSelected([]);
|
||||
resetRowSelection(true);
|
||||
resetRowSelection(false);
|
||||
}, [getCurrentFilter(moduleName)]);
|
||||
|
||||
useEffect(() => {
|
||||
if (filter !== undefined) {
|
||||
const fetchData = async () => {
|
||||
await fetchCountryData(field.action, filter);
|
||||
}
|
||||
fetchData();
|
||||
}
|
||||
|
||||
}, [filter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dataLoaded && CountryDataTable.data !== undefined) {
|
||||
setLocalData(CountryDataTable.data);
|
||||
}
|
||||
}, [dataLoaded]);
|
||||
|
||||
const handleSelection = useCallback((state) => {
|
||||
//based on the current page and the rows per page we get the rows that are selected
|
||||
const {selectedCount, selectedRows, allSelected, allRowsSelected} = state;
|
||||
let rows = [];
|
||||
if (allSelected) {
|
||||
rows = selectedRows.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage);
|
||||
setRowsSelected(rows);
|
||||
} else {
|
||||
setRowsSelected(selectedRows);
|
||||
}
|
||||
}, [currentPage, rowsPerPage, visualData]);
|
||||
|
||||
useEffect(() => {
|
||||
let FilterColumns = field.columns.map(buildColumn);
|
||||
// Find the index of the 'action' column
|
||||
const actionIndex = FilterColumns.findIndex(column => column.column === 'action');
|
||||
|
||||
|
||||
// If 'filter' equals 'regions' and 'action' column exists, then do the rearrangement
|
||||
if (filter === 'regions' && actionIndex !== -1) {
|
||||
const actionColumn = FilterColumns[actionIndex];
|
||||
|
||||
// Remove 'action' column from its current place
|
||||
FilterColumns.splice(actionIndex, 1);
|
||||
const emptyColumn = {
|
||||
name: '',
|
||||
selector: '',
|
||||
sortable: false,
|
||||
omit: false,
|
||||
searchable: false,
|
||||
};
|
||||
// Push 'action' column to the end of the array
|
||||
FilterColumns.push(emptyColumn, actionColumn);
|
||||
}
|
||||
setColumns(FilterColumns);
|
||||
const generatedVisualData = (localData || [])
|
||||
.filter((row) => {
|
||||
return Object.values(row).some((val) => ((val ?? '').toString().toLowerCase()).includes(searchTerm.toLowerCase()));
|
||||
}).map((row) => {
|
||||
const newRow = {...row};
|
||||
columns.forEach((column) => {
|
||||
newRow[column.column] = row[column.column];
|
||||
});
|
||||
if (filter === 'regions') {
|
||||
let showBlockButton = (newRow.region_count - newRow.blocked_count) > 0;
|
||||
let showAllowButton = (newRow.blocked_count > 0);
|
||||
newRow.action = generateActionButtons(newRow.iso2_code, newRow.country_name, newRow.region, showBlockButton, showAllowButton);
|
||||
} else if (filter === 'countries') {
|
||||
newRow.action = generateActionButtons(newRow.iso2_code, newRow.country_name, newRow.region);
|
||||
} else {
|
||||
newRow.action = generateActionButtons(newRow.iso2_code, newRow.status, newRow.region);
|
||||
}
|
||||
newRow.flag = generateFlag(newRow.iso2_code, newRow.country_name);
|
||||
if (newRow.status) {
|
||||
newRow.status = __(newRow.status.charAt(0).toUpperCase() + newRow.status.slice(1), 'really-simple-ssl');
|
||||
if ('regions' === filter) {
|
||||
// So i all is blocked we don't want to show the count also if all are allowed we don't want to show the count
|
||||
if (newRow.blocked_count === newRow.region_count || newRow.blocked_count === 0) {
|
||||
newRow.status = newRow.status;
|
||||
|
||||
} else {
|
||||
newRow.status = newRow.status + ' (' + newRow.blocked_count + '/ ' + newRow.region_count + ')';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return newRow;
|
||||
});
|
||||
setVisualData(generatedVisualData);
|
||||
}, [localData, searchTerm]);
|
||||
|
||||
useEffect(() => {
|
||||
if ( rowsSelected.length === 0 ) {
|
||||
resetRowSelection
|
||||
}
|
||||
}, [rowsSelected]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rsssl-container">
|
||||
<div>
|
||||
{/* reserved for left side buttons */}
|
||||
</div>
|
||||
<div className="rsssl-search-bar">
|
||||
<div className="rsssl-search-bar__inner">
|
||||
<div className="rsssl-search-bar__icon"></div>
|
||||
<input
|
||||
type="text"
|
||||
className="rsssl-search-bar__input"
|
||||
placeholder={__("Search", "really-simple-ssl")}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{rowsSelected.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
}}
|
||||
>
|
||||
<div className={"rsssl-multiselect-datatable-form rsssl-primary"}>
|
||||
<div>
|
||||
{__("You have selected %s rows", "really-simple-ssl").replace('%s', rowsSelected.length)}
|
||||
</div>
|
||||
<div className="rsssl-action-buttons">
|
||||
{filter === 'countries' && (
|
||||
<>
|
||||
<ActionButton
|
||||
onClick={() => blockCountryByCode(rowsSelected)} className="button-primary">
|
||||
{__("Block", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</>
|
||||
)}
|
||||
{filter === 'regions' && (
|
||||
<>
|
||||
<ActionButton
|
||||
onClick={() => allowRegionByCode(rowsSelected)} className="button-secondary">
|
||||
{__("Allow", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={() => blockRegionByCode(rowsSelected)} className="button-primary">
|
||||
{__("Block", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</>
|
||||
)}
|
||||
{filter === 'blocked' && (
|
||||
<ActionButton
|
||||
onClick={() => allowCountryByCode(rowsSelected)}>
|
||||
{__("Allow", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{DataTable &&
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={visualData}
|
||||
dense
|
||||
pagination={true}
|
||||
paginationComponentOptions={{
|
||||
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
|
||||
rangeSeparatorText: __('of', 'really-simple-ssl'),
|
||||
noRowsPerPage: false,
|
||||
selectAllRowsItem: false,
|
||||
selectAllRowsItemText: __('All', 'really-simple-ssl'),
|
||||
|
||||
}}
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
selectableRows={true}
|
||||
paginationPerPage={rowsPerPage}
|
||||
onChangePage={handlePageChange}
|
||||
onChangeRowsPerPage={handlePerRowsChange}
|
||||
onSelectedRowsChange={handleSelection}
|
||||
clearSelectedRows={rowCleared}
|
||||
theme="really-simple-plugins"
|
||||
customStyles={customStyles}
|
||||
>
|
||||
</DataTable> }
|
||||
{!getFieldValue('enable_firewall') && (
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay"><span
|
||||
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Restrict access from specific countries or continents. You can also allow only specific countries.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GeoDatatable;
|
||||
@@ -0,0 +1,156 @@
|
||||
import {useState} from '@wordpress/element';
|
||||
import Icon from "../../utils/Icon";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
TextControl
|
||||
} from "@wordpress/components";
|
||||
import {__} from "@wordpress/i18n";
|
||||
import FieldsData from "../FieldsData";
|
||||
import WhiteListTableStore from "./WhiteListTableStore";
|
||||
|
||||
const TrustIpAddressModal = (props) => {
|
||||
const { note, setNote, ipAddress, setIpAddress, maskError, setDataLoaded, dataLoaded, updateRow, resetRange} = WhiteListTableStore();
|
||||
const [rangeDisplay, setRangeDisplay] = useState(false);
|
||||
const {showSavedSettingsNotice} = FieldsData();
|
||||
|
||||
//we add a function to handle the range fill
|
||||
const handleRangeFill = () => {
|
||||
//we toggle the range display.
|
||||
setRangeDisplay(!rangeDisplay);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
// we check if statusSelected is not empty
|
||||
if (ipAddress && maskError === false) {
|
||||
await updateRow(ipAddress, note, props.status ,props.filter).then((response) => {
|
||||
if (response.success) {
|
||||
showSavedSettingsNotice(response.message);
|
||||
//we fetch the data again
|
||||
setDataLoaded(false);
|
||||
|
||||
} else {
|
||||
showSavedSettingsNotice(response.message, 'error');
|
||||
}
|
||||
});
|
||||
//we clear the input
|
||||
resetRange();
|
||||
//we close the modal
|
||||
props.onRequestClose();
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
// Reset all local state
|
||||
setRangeDisplay(false);
|
||||
resetRange();
|
||||
|
||||
// Close the modal
|
||||
props.onRequestClose();
|
||||
}
|
||||
if (!props.isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const changeHandler = (e) => {
|
||||
if (e.length > 0) {
|
||||
setIpAddress(e);
|
||||
} else {
|
||||
resetRange()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={__("Add IP Address", "really-simple-ssl")}
|
||||
shouldCloseOnClickOutside={true}
|
||||
shouldCloseOnEsc={true}
|
||||
overlayClassName="rsssl-modal-overlay"
|
||||
className="rsssl-modal"
|
||||
onRequestClose={props.onRequestClose}
|
||||
>
|
||||
<div className="modal-content">
|
||||
<div className="modal-body"
|
||||
style={{
|
||||
padding: "0.5em",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "95%",
|
||||
height: "100%",
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
<div style={{position: 'relative'}}>
|
||||
<label
|
||||
htmlFor={'ip-address'}
|
||||
className={'rsssl-label'}
|
||||
>{__('IP Address', 'really-simple-ssl')}</label>
|
||||
<TextControl
|
||||
id="ip-address"
|
||||
name="ip-address"
|
||||
onChange={changeHandler}
|
||||
value={ipAddress}
|
||||
/>
|
||||
<div className="rsssl-ip-verified">
|
||||
{Boolean(!maskError && ipAddress.length > 0)
|
||||
? <Icon name='circle-check' color={'green'}/>
|
||||
: <Icon name='circle-times' color={'red'}/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor={'note'}
|
||||
className={'rsssl-label'}
|
||||
>{__('Notes', 'really-simple-ssl')}</label>
|
||||
<input
|
||||
name={'note'}
|
||||
id={'note'}
|
||||
type={'text'}
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
{/*//we add two buttons here for add row and cancel*/}
|
||||
<div
|
||||
className={'rsssl-grid-item-footer'}
|
||||
style
|
||||
={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
padding: '1em',
|
||||
}
|
||||
}
|
||||
>
|
||||
<Button
|
||||
isSecondary
|
||||
onClick={handleCancel}
|
||||
style={{ marginRight: '10px' }}
|
||||
|
||||
>
|
||||
{__("Cancel", "really-simple-ssl")}
|
||||
</Button>
|
||||
<Button
|
||||
isPrimary
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{__("Add", "really-simple-ssl")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default TrustIpAddressModal;
|
||||
@@ -0,0 +1,423 @@
|
||||
import { useEffect, useState, useCallback } from '@wordpress/element';
|
||||
import DataTable, {createTheme} from "react-data-table-component";
|
||||
import FieldsData from "../FieldsData";
|
||||
import WhiteListTableStore from "./WhiteListTableStore";
|
||||
import Flag from "../../utils/Flag/Flag";
|
||||
import {__} from '@wordpress/i18n';
|
||||
import useFields from "../FieldsData";
|
||||
import AddButton from "./AddButton";
|
||||
import TrustIpAddressModal from "./TrustIpAddressModal";
|
||||
|
||||
const WhiteListDatatable = (props) => {
|
||||
const {
|
||||
WhiteListTable,
|
||||
fetchWhiteListData,
|
||||
processing,
|
||||
ipAddress,
|
||||
addRow,
|
||||
removeRow,
|
||||
pagination,
|
||||
addRegion,
|
||||
removeRegion,
|
||||
resetRow,
|
||||
rowCleared,
|
||||
} = WhiteListTableStore();
|
||||
|
||||
const {showSavedSettingsNotice, saveFields} = FieldsData();
|
||||
|
||||
const [rowsSelected, setRowsSelected] = useState([]);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [tableHeight, setTableHeight] = useState(600); // Starting height
|
||||
const rowHeight = 50; // Height of each row.
|
||||
const moduleName = 'rsssl-group-filter-geo_block_list_white_listing';
|
||||
const {fields, fieldAlreadyEnabled, getFieldValue} = useFields();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [DataTable, setDataTable] = useState(null);
|
||||
const [theme, setTheme] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
import('react-data-table-component').then((module) => {
|
||||
const { default: DataTable, createTheme } = module;
|
||||
setDataTable(() => DataTable);
|
||||
setTheme(() => createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light'));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
const handlePerRowsChange = (newRowsPerPage) => {
|
||||
setRowsPerPage(newRowsPerPage);
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a column configuration object.
|
||||
*
|
||||
* @param {object} column - The column object.
|
||||
* @param {string} column.name - The name of the column.
|
||||
* @param {boolean} column.sortable - Whether the column is sortable.
|
||||
* @param {boolean} column.searchable - Whether the column is searchable.
|
||||
* @param {number} column.width - The width of the column.
|
||||
* @param {boolean} column.visible - Whether the column is visible.
|
||||
* @param {string} column.column - The column identifier.
|
||||
*
|
||||
* @returns {object} The column configuration object.
|
||||
*/
|
||||
const buildColumn = useCallback((column) => ({
|
||||
//if the filter is set to region and the columns = status we do not want to show the column
|
||||
name: column.name,
|
||||
sortable: column.sortable,
|
||||
searchable: column.searchable,
|
||||
width: column.width,
|
||||
visible: column.visible,
|
||||
column: column.column,
|
||||
selector: row => row[column.column],
|
||||
}), []);
|
||||
let field = props.field;
|
||||
const columns = field.columns.map(buildColumn);
|
||||
|
||||
const searchableColumns = columns
|
||||
.filter(column => column.searchable)
|
||||
.map(column => column.column);
|
||||
|
||||
useEffect(() => {
|
||||
setRowsSelected([]);
|
||||
}, [WhiteListTable]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchWhiteListData(field.action);
|
||||
|
||||
}, [fieldAlreadyEnabled('enable_firewall')]);
|
||||
|
||||
let enabled = getFieldValue('enable_firewall');
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
saveFields(false, false)
|
||||
};
|
||||
}, [enabled]);
|
||||
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light');
|
||||
|
||||
const handleSelection = useCallback((state) => {
|
||||
//based on the current page and the rows per page we get the rows that are selected
|
||||
const {selectedCount, selectedRows, allSelected, allRowsSelected} = state;
|
||||
let rows = [];
|
||||
if (allSelected) {
|
||||
rows = selectedRows.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage);
|
||||
setRowsSelected(rows);
|
||||
} else {
|
||||
setRowsSelected(selectedRows);
|
||||
}
|
||||
}, [currentPage, rowsPerPage]);
|
||||
|
||||
const allowRegionByCode = useCallback(async (code, regionName = '') => {
|
||||
if (Array.isArray(code)) {
|
||||
const ids = code.map(item => ({
|
||||
iso2_code: item.iso2_code,
|
||||
country_name: item.country_name
|
||||
}));
|
||||
ids.forEach((id) => {
|
||||
removeRegion(id.iso2_code).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
});
|
||||
setRowsSelected([]);
|
||||
await fetchWhiteListData(field.action);
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
await removeRegion(code).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
}
|
||||
}, [removeRegion]);
|
||||
|
||||
const allowById = useCallback((id) => {
|
||||
//We check if the id is an array
|
||||
if (Array.isArray(id)) {
|
||||
//We get all the iso2 codes and names from the array
|
||||
const ids = id.map(item => ({
|
||||
id: item.id,
|
||||
}));
|
||||
//we loop through the ids and allow them one by one
|
||||
ids.forEach((id) => {
|
||||
resetRow(id.id).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
});
|
||||
fetchWhiteListData(field.action);
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
resetRow(id).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
fetchWhiteListData(field.action);
|
||||
});
|
||||
}
|
||||
}, [resetRow]);
|
||||
|
||||
const blockRegionByCode = useCallback(async (code, region = '') => {
|
||||
if (Array.isArray(code)) {
|
||||
const ids = code.map(item => ({
|
||||
iso2_code: item.iso2_code,
|
||||
country_name: item.country_name
|
||||
}));
|
||||
ids.forEach((id) => {
|
||||
addRegion(id.iso2_code).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
});
|
||||
setRowsSelected([]);
|
||||
await fetchWhiteListData(field.action);
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
await addRegion(code).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
}
|
||||
|
||||
}, [addRegion]);
|
||||
|
||||
const allowCountryByCode = useCallback(async (code) => {
|
||||
if (Array.isArray(code)) {
|
||||
const ids = code.map(item => ({
|
||||
iso2_code: item.iso2_code,
|
||||
country_name: item.country_name
|
||||
}));
|
||||
//we loop through the ids and allow them one by one
|
||||
ids.forEach((id) => {
|
||||
removeRow(id.iso2_code).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
});
|
||||
setRowsSelected([]);
|
||||
await fetchWhiteListData(field.action);
|
||||
} else {
|
||||
await removeRow(code).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
}
|
||||
|
||||
}, [removeRow]);
|
||||
|
||||
const blockCountryByCode = useCallback(async (code, name) => {
|
||||
if (Array.isArray(code)) {
|
||||
//We get all the iso2 codes and names from the array
|
||||
const ids = code.map(item => ({
|
||||
iso2_code: item.iso2_code,
|
||||
country_name: item.country_name
|
||||
}));
|
||||
//we loop through the ids and block them one by one
|
||||
ids.forEach((id) => {
|
||||
addRow(id.iso2_code, id.country_name).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
});
|
||||
setRowsSelected([]);
|
||||
} else {
|
||||
await addRow(code, name).then((result) => {
|
||||
showSavedSettingsNotice(result.message);
|
||||
});
|
||||
}
|
||||
|
||||
}, [addRow]);
|
||||
|
||||
const data = {...WhiteListTable.data};
|
||||
|
||||
const generateFlag = useCallback((flag, title) => (
|
||||
<>
|
||||
<Flag
|
||||
countryCode={flag}
|
||||
style={{
|
||||
fontSize: '2em',
|
||||
}}
|
||||
title={title}
|
||||
/>
|
||||
</>
|
||||
), []);
|
||||
|
||||
const ActionButton = ({ onClick, children, className }) => (
|
||||
// <div className={`rsssl-action-buttons__inner`}>
|
||||
<button
|
||||
className={`button ${className} rsssl-action-buttons__button`}
|
||||
onClick={onClick}
|
||||
disabled={processing}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
// </div>
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
setModalOpen(false);
|
||||
}
|
||||
|
||||
const handleOpen = () => {
|
||||
setModalOpen(true);
|
||||
}
|
||||
|
||||
const generateActionButtons = useCallback((id) => {
|
||||
return (<div className="rsssl-action-buttons">
|
||||
<ActionButton
|
||||
onClick={() => allowById(id)} className="button-red">
|
||||
{__("Reset", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</div>)
|
||||
}, [moduleName, allowById, blockRegionByCode, allowRegionByCode, blockCountryByCode, allowCountryByCode]);
|
||||
|
||||
|
||||
|
||||
for (const key in data) {
|
||||
const dataItem = {...data[key]};
|
||||
dataItem.action = generateActionButtons(dataItem.id);
|
||||
dataItem.flag = generateFlag(dataItem.iso2_code, dataItem.country_name);
|
||||
dataItem.status = __(dataItem.status = dataItem.status.charAt(0).toUpperCase() + dataItem.status.slice(1), 'really-simple-ssl');
|
||||
data[key] = dataItem;
|
||||
}
|
||||
|
||||
let paginationSet;
|
||||
paginationSet = typeof pagination !== 'undefined';
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(data).length === 0 ) {
|
||||
setTableHeight(100); // Adjust depending on your UI measurements
|
||||
} else {
|
||||
setTableHeight(rowHeight * (paginationSet ? pagination.perPage + 2 : 12)); // Adjust depending on your UI measurements
|
||||
}
|
||||
|
||||
}, [paginationSet, pagination?.perPage, data]);
|
||||
|
||||
useEffect(() => {
|
||||
const filteredData = Object.entries(data)
|
||||
.filter(([_, dataItem]) => {
|
||||
return Object.values(dataItem).some(val => ((val ?? '').toString().toLowerCase().includes(searchTerm.toLowerCase())));
|
||||
})
|
||||
.map(([key, dataItem]) => {
|
||||
const newItem = { ...dataItem };
|
||||
newItem.action = generateActionButtons(newItem.id);
|
||||
newItem.flag = generateFlag(newItem.iso2_code, newItem.country_name);
|
||||
newItem.status = __(newItem.status = newItem.status.charAt(0).toUpperCase() + newItem.status.slice(1), 'really-simple-ssl');
|
||||
return [key, newItem];
|
||||
})
|
||||
.reduce((obj, [key, val]) => ({ ...obj, [key]: val }), {});
|
||||
}, [searchTerm, data]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<TrustIpAddressModal
|
||||
isOpen={modalOpen}
|
||||
onRequestClose={handleClose}
|
||||
value={ipAddress}
|
||||
status={'trusted'}
|
||||
>
|
||||
</TrustIpAddressModal>
|
||||
<div className="rsssl-container">
|
||||
{/*display the add button on left side*/}
|
||||
<AddButton
|
||||
moduleName={moduleName}
|
||||
handleOpen={handleOpen}
|
||||
processing={processing}
|
||||
blockedText={__("Block IP Address", "really-simple-ssl")}
|
||||
allowedText={__("Trust IP Address", "really-simple-ssl")}
|
||||
/>
|
||||
<div className="rsssl-search-bar">
|
||||
<div className="rsssl-search-bar__inner">
|
||||
<div className="rsssl-search-bar__icon"></div>
|
||||
<input
|
||||
type="text"
|
||||
className="rsssl-search-bar__input"
|
||||
placeholder={__("Search", "really-simple-ssl")}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{rowsSelected.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
}}
|
||||
>
|
||||
<div className={"rsssl-multiselect-datatable-form rsssl-primary"}>
|
||||
<div>
|
||||
{__("You have selected %s rows", "really-simple-ssl").replace('%s', rowsSelected.length)}
|
||||
</div>
|
||||
<div className="rsssl-action-buttons">
|
||||
<>
|
||||
<ActionButton
|
||||
onClick={() => allowById(rowsSelected)} className="button-red">
|
||||
{__("Reset", "really-simple-ssl")}
|
||||
</ActionButton>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{DataTable &&
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={Object.values(data).filter((row) => {
|
||||
return Object.values(row).some((val) => ((val ?? '').toString().toLowerCase()).includes(searchTerm.toLowerCase()));
|
||||
})}
|
||||
dense
|
||||
pagination={true}
|
||||
paginationComponentOptions={{
|
||||
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
|
||||
rangeSeparatorText: __('of', 'really-simple-ssl'),
|
||||
noRowsPerPage: false,
|
||||
selectAllRowsItem: false,
|
||||
selectAllRowsItemText: __('All', 'really-simple-ssl'),
|
||||
|
||||
}}
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
selectableRows={true}
|
||||
clearSelectedRows={rowCleared}
|
||||
paginationPerPage={rowsPerPage}
|
||||
onChangePage={handlePageChange}
|
||||
onChangeRowsPerPage={handlePerRowsChange}
|
||||
onSelectedRowsChange={handleSelection}
|
||||
theme="really-simple-plugins"
|
||||
customStyles={customStyles}
|
||||
/> }
|
||||
{!enabled && (
|
||||
<div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay"><span
|
||||
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Here you can add IP addresses that should never be blocked by region restrictions.', 'really-simple-ssl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default WhiteListDatatable;
|
||||
@@ -0,0 +1,202 @@
|
||||
/* Creates A Store For Risk Data using Zustand */
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
|
||||
const WhiteListTableStore = create((set, get) => ({
|
||||
|
||||
processing: false,
|
||||
processing_block: false,
|
||||
dataLoaded: false,
|
||||
dataLoaded_block: false,
|
||||
pagination: {},
|
||||
dataActions: {},
|
||||
WhiteListTable: [],
|
||||
BlockListData: [],
|
||||
rowCleared: false,
|
||||
maskError: false,
|
||||
ipAddress: '',
|
||||
note: '',
|
||||
|
||||
|
||||
fetchWhiteListData: async (action) => {
|
||||
//we check if the processing is already true, if so we return
|
||||
set({processing: true});
|
||||
set({dataLoaded: false});
|
||||
set({rowCleared: true});
|
||||
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action
|
||||
);
|
||||
//now we set the EventLog
|
||||
if (response && response.request_success) {
|
||||
set({WhiteListTable: response, dataLoaded: true, processing: false, pagination: response.pagination});
|
||||
}
|
||||
set({ rowCleared: true });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
set({processing: false});
|
||||
set({rowCleared: false});
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
fetchData: async (action, filter) => {
|
||||
//we check if the processing is already true, if so we return
|
||||
set({processing_block: true});
|
||||
set({rowCleared: true});
|
||||
|
||||
try {
|
||||
const response = await rsssl_api.doAction(
|
||||
action,
|
||||
{
|
||||
filterValue: filter
|
||||
}
|
||||
);
|
||||
//now we set the EventLog
|
||||
if (response && response.request_success) {
|
||||
set({BlockListData: response, dataLoaded: true, processing: false, pagination: response.pagination});
|
||||
}
|
||||
set({ rowCleared: true });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
set({dataLoaded_block: true})
|
||||
set({processing_block: false});
|
||||
set({rowCleared: false});
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
resetRow: async (id, dataActions) => {
|
||||
set({processing: true});
|
||||
let data = {
|
||||
id: id
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_reset_ip', data);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
// Potentially notify the user of success, if needed.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Handle any unsuccessful response if needed.
|
||||
return { success: false, message: response?.message || 'Failed to reset Ip', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Notify the user of an error.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
}
|
||||
,
|
||||
updateRow: async (ip, note, status, filter) => {
|
||||
set({processing: true});
|
||||
let data = {
|
||||
ip_address: ip,
|
||||
note: note,
|
||||
status: status
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_add_white_list_ip', data);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
await get().fetchWhiteListData('rsssl_geo_white_list');
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Handle any unsuccessful response if needed.
|
||||
return { success: false, message: response?.message || 'Failed to add Ip', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Notify the user of an error.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
|
||||
removeRow: async (country, dataActions) => {
|
||||
set({processing: true});
|
||||
let data = {
|
||||
country_code: country
|
||||
};
|
||||
try {
|
||||
const response = await rsssl_api.doAction('geo_block_remove_blocked_country', data);
|
||||
// Consider checking the response structure for any specific success or failure signals
|
||||
if (response && response.request_success) {
|
||||
await get().fetchCountryData('rsssl_geo_white_list');
|
||||
await get().fetchData('rsssl_geo_block_list', {filterValue: 'all'});
|
||||
// Potentially notify the user of success, if needed.
|
||||
return { success: true, message: response.message, response };
|
||||
} else {
|
||||
// Handle any unsuccessful response if needed.
|
||||
return { success: false, message: response?.message || 'Failed to remove country', response };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Notify the user of an error.
|
||||
return { success: false, message: 'Error occurred', error: e };
|
||||
} finally {
|
||||
set({processing: false});
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* This function sets the ip address and is used by Cidr and IpAddressInput
|
||||
*/
|
||||
setIpAddress: (ipAddress) => {
|
||||
if(ipAddress.length === 0) {
|
||||
return;
|
||||
}
|
||||
let ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$|^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,4}|((25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9]))|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9]))$/;
|
||||
if (ipAddress.includes('/')) {
|
||||
let finalIp = '';
|
||||
// Split the input into IP and CIDR mask
|
||||
let [ip, mask] = ipAddress.split('/');
|
||||
//if , we change it to .
|
||||
ip = ip.replace(/,/g, '.');
|
||||
if (mask.length <= 0 ) {
|
||||
if (!ipRegex.test(ip)) {
|
||||
set({maskError: true});
|
||||
} else {
|
||||
set({maskError: false});
|
||||
}
|
||||
finalIp = `${ip}/${mask}`;
|
||||
} else {
|
||||
finalIp = mask ? `${ip}/${mask}` : ip;
|
||||
}
|
||||
set({ ipAddress: finalIp })
|
||||
} else {
|
||||
if (!ipRegex.test(ipAddress)) {
|
||||
set({maskError: true});
|
||||
} else {
|
||||
set({maskError: false});
|
||||
}
|
||||
set({ ipAddress: ipAddress.replace(/,/g, '.') })
|
||||
}
|
||||
},
|
||||
|
||||
setNote: (note) => {
|
||||
set({note});
|
||||
},
|
||||
|
||||
resetRange: () => {
|
||||
set({inputRangeValidated: false});
|
||||
set({highestIP: ''});
|
||||
set({lowestIP: ''});
|
||||
set({ipAddress: ''});
|
||||
set({maskError: false});
|
||||
},
|
||||
|
||||
setDataLoaded: (dataLoaded) => {
|
||||
set({dataLoaded});
|
||||
set({dataLoaded_block: dataLoaded});
|
||||
}
|
||||
|
||||
}));
|
||||
|
||||
export default WhiteListTableStore;
|
||||
@@ -0,0 +1,34 @@
|
||||
import Icon from "../utils/Icon";
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import DOMPurify from "dompurify";
|
||||
/**
|
||||
* Render a help notice in the sidebar
|
||||
*/
|
||||
const Help = (props) => {
|
||||
let notice = {...props.help};
|
||||
if ( !notice.title ){
|
||||
notice.title = notice.text;
|
||||
notice.text = false;
|
||||
}
|
||||
let openStatus = props.noticesExpanded ? 'open' : '';
|
||||
//we can use notice.linked_field to create a visual link to the field.
|
||||
|
||||
let target = notice.url && notice.url.indexOf("really-simple-ssl.com") !==-1 ? "_blank" : '_self';
|
||||
return (
|
||||
<div>
|
||||
{ notice.title && notice.text &&
|
||||
<details className={"rsssl-wizard-help-notice rsssl-" + notice.label.toLowerCase()} open={openStatus}>
|
||||
<summary>{notice.title} <Icon name='chevron-down' /></summary>
|
||||
{/*some notices contain html, like for the htaccess notices. A title is required for those options, otherwise the text becomes the title. */}
|
||||
<div dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(notice.text)}}></div>
|
||||
{notice.url && <div className="rsssl-help-more-info"><a target={target} href={notice.url}>{__("More info", "really-simple-ssl")}</a></div>}
|
||||
</details>
|
||||
}
|
||||
{ notice.title && !notice.text &&
|
||||
<div className={"rsssl-wizard-help-notice rsssl-" + notice.label.toLowerCase()}><p>{notice.title}</p></div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Help
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useState, useEffect, memo } from "@wordpress/element";
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import useFields from "../FieldsData";
|
||||
import AutoCompleteControl from "../AutoComplete/AutoCompleteControl";
|
||||
import useHostData from "./HostData";
|
||||
import { __ } from "@wordpress/i18n";
|
||||
import autoCompleteSharedTheme from "../../utils/autoCompleteTheme";
|
||||
const Host = ({ field, showDisabledWhenSaving = true }) => {
|
||||
const { updateField, setChangedField, saveFields, handleNextButtonDisabled } = useFields();
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const { fetchHosts, hosts, hostsLoaded } = useHostData();
|
||||
|
||||
useEffect(() => {
|
||||
if (!hostsLoaded) {
|
||||
fetchHosts();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
handleNextButtonDisabled(disabled);
|
||||
}, [disabled]);
|
||||
|
||||
const onChangeHandler = async (fieldValue) => {
|
||||
if (showDisabledWhenSaving) {
|
||||
setDisabled(true);
|
||||
}
|
||||
updateField(field.id, fieldValue);
|
||||
setChangedField(field.id, fieldValue);
|
||||
await saveFields(true, false);
|
||||
setDisabled(false);
|
||||
};
|
||||
|
||||
let loadedHosts = hostsLoaded ? hosts : [];
|
||||
let options = [];
|
||||
let item = {
|
||||
label: __('Optional - Select your hosting provider.', 'really-simple-ssl'),
|
||||
value: '',
|
||||
};
|
||||
if (field.value.length === 0) {
|
||||
options.push(item);
|
||||
}
|
||||
for (let key in loadedHosts) {
|
||||
if (loadedHosts.hasOwnProperty(key)) {
|
||||
let item = {};
|
||||
item.label = loadedHosts[key].name;
|
||||
item.value = key;
|
||||
options.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={autoCompleteSharedTheme}>
|
||||
<AutoCompleteControl
|
||||
className="rsssl-select"
|
||||
field={field}
|
||||
label={field.label}
|
||||
onChange={(fieldValue) => onChangeHandler(fieldValue)}
|
||||
value={field.value}
|
||||
options={options}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Host);
|
||||
@@ -0,0 +1,25 @@
|
||||
import {create} from 'zustand';
|
||||
import * as rsssl_api from "../../utils/api";
|
||||
const useHostData = create(( set, get ) => ({
|
||||
hosts: [],
|
||||
hostsLoaded:false,
|
||||
fetchHosts: async ( id ) => {
|
||||
try {
|
||||
const response = await rsssl_api.doAction('get_hosts', { id: id });
|
||||
|
||||
// Handle the response
|
||||
if ( !response ) {
|
||||
console.error('No response received from the server.');
|
||||
return;
|
||||
}
|
||||
let hosts = response.hosts;
|
||||
// Set the roles state with formatted data
|
||||
set({hosts: hosts,hostsLoaded:true });
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
export default useHostData;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import useLearningMode from "./LearningModeData";
|
||||
const ChangeStatus = (props) => {
|
||||
const {updateStatus} = useLearningMode();
|
||||
|
||||
let statusClass = props.item.status==1 ? 'button button-primary rsssl-status-allowed' : 'button button-default rsssl-status-revoked';
|
||||
let label = props.item.status==1 ? __("Revoke", "really-simple-ssl") : __("Allow", "really-simple-ssl");
|
||||
return (
|
||||
<button onClick={ () => updateStatus( props.item.status, props.item, props.field.id ) } className={statusClass}>{label}</button>
|
||||
)
|
||||
}
|
||||
export default ChangeStatus
|
||||
@@ -0,0 +1,16 @@
|
||||
import useLearningMode from "./LearningModeData";
|
||||
import {__} from "@wordpress/i18n";
|
||||
|
||||
const Delete = (props) => {
|
||||
const {deleteData} = useLearningMode();
|
||||
|
||||
return (
|
||||
<button type="button" className="button button-red rsssl-learning-mode-delete" onClick={ () => deleteData( props.item, props.field.id ) }>
|
||||
{/*<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" height="16" >*/}
|
||||
{/* <path fill="#000000" d="M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z"/>*/}
|
||||
{/*</svg>*/}
|
||||
{__("Delete", "really-simple-ssl")}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
export default Delete
|
||||
@@ -0,0 +1,400 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {useState,useEffect} from '@wordpress/element';
|
||||
import ChangeStatus from "./ChangeStatus";
|
||||
import Delete from "./Delete";
|
||||
import Icon from "../../utils/Icon";
|
||||
import useFields from "./../FieldsData";
|
||||
import UseLearningMode from "./LearningModeData";
|
||||
import {Button} from "@wordpress/components";
|
||||
import React from "react";
|
||||
import ManualCspAdditionModal from "./ManualCspAdditionModal";
|
||||
import AddButton from "../GeoBlockList/AddButton";
|
||||
import ManualCspAddition from "./ManualCspAddition";
|
||||
|
||||
const LearningMode = (props) => {
|
||||
|
||||
const {updateField, getFieldValue, getField, setChangedField, highLightField, saveFields} = useFields();
|
||||
const {fetchLearningModeData, learningModeData, dataLoaded, updateStatus, deleteData} = UseLearningMode();
|
||||
const {manualAdditionProcessing} = ManualCspAddition();
|
||||
|
||||
//used to show if a feature is already enforced by a third party
|
||||
const [enforcedByThirdparty, setEnforcedByThirdparty] = useState(0);
|
||||
//toggle from enforced to not enforced
|
||||
const [enforce, setEnforce] = useState(0);
|
||||
//toggle from learning mode to not learning mode
|
||||
const [learningMode, setLearningMode] = useState(0);
|
||||
//set learning mode to completed
|
||||
const [learningModeCompleted, setLearningModeCompleted] = useState(0);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
//check if learningmode has been enabled at least once
|
||||
const [lmEnabledOnce, setLmEnabledOnce] = useState(0);
|
||||
//filter the data
|
||||
const [filterValue, setFilterValue] = useState(-1);
|
||||
//the value that is used to enable or disable this feature. On or of.
|
||||
const [controlField, setControlField] = useState(false);
|
||||
// the value that is used to select and deselect rows
|
||||
const [rowsSelected, setRowsSelected] = useState([]);
|
||||
const [rowCleared, setRowCleared] = useState(false);
|
||||
|
||||
const [DataTable, setDataTable] = useState(null);
|
||||
const [theme, setTheme] = useState(null);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
useEffect( () => {
|
||||
import('react-data-table-component').then(({ default: DataTable, createTheme }) => {
|
||||
setDataTable(() => DataTable);
|
||||
setTheme(() => createTheme('really-simple-plugins', {
|
||||
divider: {
|
||||
default: 'transparent',
|
||||
},
|
||||
}, 'light'));
|
||||
});
|
||||
|
||||
}, []);
|
||||
|
||||
|
||||
/**
|
||||
* Styling
|
||||
*/
|
||||
const conditionalRowStyles = [
|
||||
{
|
||||
when: row => row.status ==0,
|
||||
classNames: ['rsssl-datatables-revoked'],
|
||||
},
|
||||
];
|
||||
|
||||
const customStyles = {
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for head cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: '0', // override the cell padding for data cells
|
||||
paddingRight: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
;
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*/
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
await fetchLearningModeData(props.field.id);
|
||||
let controlField = getField(props.field.control_field );
|
||||
let enforced_by_thirdparty = controlField.value === 'enforced-by-thirdparty';
|
||||
let enforce = enforced_by_thirdparty || controlField.value === 'enforce';
|
||||
|
||||
setControlField(controlField);
|
||||
setEnforcedByThirdparty(enforced_by_thirdparty);
|
||||
setLearningModeCompleted(controlField.value==='completed');
|
||||
setHasError(controlField.value==='error');
|
||||
setLmEnabledOnce(getFieldValue(props.field.control_field+'_lm_enabled_once'))
|
||||
setEnforce(enforce);
|
||||
setLearningMode(controlField.value === 'learning_mode');
|
||||
}
|
||||
run();
|
||||
}, [enforce, learningMode] );
|
||||
|
||||
const toggleEnforce = async (e, enforceValue) => {
|
||||
e.preventDefault();
|
||||
//enforce this setting
|
||||
let controlFieldValue = enforceValue==1 ? 'enforce' : 'disabled';
|
||||
setEnforce(enforceValue);
|
||||
setLearningModeCompleted(0);
|
||||
setLearningMode(0);
|
||||
setChangedField(controlField.id, controlFieldValue);
|
||||
updateField(controlField.id, controlFieldValue);
|
||||
await saveFields(true, false);
|
||||
//await fetchLearningModeData();
|
||||
}
|
||||
|
||||
|
||||
const toggleLearningMode = async (e) => {
|
||||
e.preventDefault();
|
||||
let lmEnabledOnceField = getField(props.field.control_field+'_lm_enabled_once');
|
||||
if ( learningMode ) {
|
||||
setLmEnabledOnce(1);
|
||||
updateField(lmEnabledOnceField.id, 1);
|
||||
}
|
||||
|
||||
let controlFieldValue;
|
||||
if ( learningMode || learningModeCompleted ) {
|
||||
setLearningMode(0);
|
||||
controlFieldValue = 'disabled';
|
||||
} else {
|
||||
setLearningMode(1);
|
||||
controlFieldValue = 'learning_mode';
|
||||
}
|
||||
setLearningModeCompleted(0);
|
||||
setChangedField(controlField.id, controlFieldValue);
|
||||
updateField(controlField.id, controlFieldValue);
|
||||
setChangedField(lmEnabledOnceField.id, lmEnabledOnceField.value);
|
||||
updateField(lmEnabledOnceField, lmEnabledOnceField.value);
|
||||
await saveFields(true, false);
|
||||
}
|
||||
|
||||
const Filter = () => (
|
||||
<>
|
||||
<select onChange={ ( e ) => setFilterValue(e.target.value) } value={filterValue}>
|
||||
<option value="-1" >{__("All", "really-simple-ssl")}</option>
|
||||
<option value="1" >{__("Allowed", "really-simple-ssl")}</option>
|
||||
<option value="0" >{__("Blocked", "really-simple-ssl")}</option>
|
||||
</select>
|
||||
</>
|
||||
);
|
||||
|
||||
let field = props.field;
|
||||
let configuringString = __(" The %s is now in report-only mode and will collect directives. This might take a while. Afterwards you can Exit, Edit and Enforce these Directives.", "really-simple-ssl").replace('%s', field.label);
|
||||
let disabledString = __("%s has been disabled.", "really-simple-ssl").replace('%s', field.label);
|
||||
let enforcedString = __("%s is enforced.", "really-simple-ssl").replace('%s', field.label);
|
||||
let enforceDisabled = !lmEnabledOnce;
|
||||
if (enforcedByThirdparty) disabledString = __("%s is already set outside Really Simple Security.", "really-simple-ssl").replace('%s', field.label);
|
||||
let highLightClass = 'rsssl-field-wrap';
|
||||
if ( highLightField===props.field.id ) {
|
||||
highLightClass = 'rsssl-field-wrap rsssl-highlight';
|
||||
}
|
||||
//build our header
|
||||
let columns = [];
|
||||
field.columns.forEach(function(item, i) {
|
||||
let newItem = {
|
||||
name: item.name,
|
||||
sortable: item.sortable,
|
||||
width: item.width,
|
||||
selector: item.column === 'documenturi' || item.column === 'method'
|
||||
? row => <span title={row[item.column]}>{row[item.column]}</span>: row => row[item.column],
|
||||
}
|
||||
columns.push(newItem);
|
||||
});
|
||||
|
||||
let data = learningModeData;
|
||||
data = data.filter(item => item.status<2);
|
||||
if (filterValue!=-1) {
|
||||
data = data.filter(item => item.status==filterValue);
|
||||
}
|
||||
for (const item of data){
|
||||
if (item.login_status) item.login_statusControl = item.login_status == 1 ? __("success", "really-simple-ssl") : __("failed", "really-simple-ssl");
|
||||
item.statusControl = <ChangeStatus item={item} field={props.field} />;
|
||||
item.deleteControl = <Delete item={item} field={props.field}/>;
|
||||
item.grouped = <div className="rsssl-action-buttons">
|
||||
<ChangeStatus item={item} field={props.field} />
|
||||
<Delete item={item} field={props.field}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
const handleMultiRowStatus = (status, selectedRows, type) => {
|
||||
selectedRows.forEach(row => {
|
||||
//the updateItemId allows us to update one specific item in a field set.
|
||||
updateStatus(status, row, type);
|
||||
});
|
||||
setRowCleared(true);
|
||||
setRowsSelected([]);
|
||||
// Reset rowCleared back to false after the DataTable has re-rendered
|
||||
setTimeout(() => setRowCleared(false), 0);
|
||||
}
|
||||
|
||||
const handleMultiRowDelete = ( selectedRows, type) => {
|
||||
selectedRows.forEach(row => {
|
||||
//the updateItemId allows us to update one specific item in a field set.
|
||||
deleteData( row, type );
|
||||
});
|
||||
setRowCleared(true);
|
||||
setRowsSelected([]);
|
||||
// Reset rowCleared back to false after the DataTable has re-rendered
|
||||
setTimeout(() => setRowCleared(false), 0);
|
||||
}
|
||||
function handleSelection(state) {
|
||||
setRowCleared(false);
|
||||
setRowsSelected(state.selectedRows);
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setModalOpen(false);
|
||||
}
|
||||
|
||||
const handleOpen = () => {
|
||||
setModalOpen(true);
|
||||
}
|
||||
|
||||
if (!DataTable || !theme) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.field.id === 'content_security_policy_source_directives' && (<>
|
||||
<ManualCspAdditionModal
|
||||
isOpen={modalOpen}
|
||||
onRequestClose={handleClose}
|
||||
status={'blocked'}
|
||||
directives={props.field.modal.options}
|
||||
parentId={props.field.id}
|
||||
/>
|
||||
<div className="rsssl-container" style={{paddingTop: "0px"}}>
|
||||
<AddButton
|
||||
handleOpen={handleOpen}
|
||||
processing={manualAdditionProcessing}
|
||||
allowedText={__("Add Entry", "really-simple-ssl")}
|
||||
/>
|
||||
</div>
|
||||
</>)}
|
||||
{!dataLoaded && <>
|
||||
<div className="rsssl-learningmode-placeholder">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</>}
|
||||
{rowsSelected.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={"rsssl-multiselect-datatable-form rsssl-primary"}
|
||||
>
|
||||
<div>
|
||||
{__("You have selected", "really-simple-ssl")} {rowsSelected.length} {__("rows", "really-simple-ssl")}
|
||||
</div>
|
||||
|
||||
<div className="rsssl-action-buttons">
|
||||
{(Number(filterValue) === -1 || Number(filterValue) === 0) &&
|
||||
<div className="rsssl-action-buttons__inner">
|
||||
<Button
|
||||
// className={"button button-red rsssl-action-buttons__button"}
|
||||
className={"button button-secondary rsssl-status-allowed rsssl-action-buttons__button"}
|
||||
onClick={() => handleMultiRowStatus(0, rowsSelected, props.field.id)}
|
||||
>
|
||||
{__('Allow', 'really-simple-ssl')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
{(Number(filterValue) === -1 || Number(filterValue) === 1) &&
|
||||
<div className="rsssl-action-buttons__inner">
|
||||
<Button
|
||||
// className={"button button-red rsssl-action-buttons__button"}
|
||||
className={"button button-primary rsssl-action-buttons__button"}
|
||||
onClick={() => handleMultiRowStatus(1, rowsSelected, props.field.id)}
|
||||
>
|
||||
{__('Revoke', 'really-simple-ssl')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
<div className="rsssl-action-buttons__inner">
|
||||
<Button
|
||||
// className={"button button-red rsssl-action-buttons__button"}
|
||||
className={"button button-red rsssl-action-buttons__button"}
|
||||
onClick={() => handleMultiRowDelete(rowsSelected, props.field.id)}
|
||||
>
|
||||
{__('Remove', 'really-simple-ssl')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{dataLoaded && <>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
dense
|
||||
pagination
|
||||
noDataComponent={__("No results", "really-simple-ssl")}
|
||||
persistTableHead
|
||||
theme={theme}
|
||||
customStyles={customStyles}
|
||||
conditionalRowStyles={conditionalRowStyles}
|
||||
paginationComponentOptions={{
|
||||
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
|
||||
rangeSeparatorText: __('of', 'really-simple-ssl'),
|
||||
noRowsPerPage: false,
|
||||
selectAllRowsItem: false,
|
||||
selectAllRowsItemText: __('All', 'really-simple-ssl'),
|
||||
}}
|
||||
selectableRows
|
||||
selectableRowsHighlight={true}
|
||||
onSelectedRowsChange={handleSelection}
|
||||
clearSelectedRows={rowCleared}
|
||||
/></>
|
||||
}
|
||||
<div className={"rsssl-learning-mode-footer"} style={{marginLeft:'0px'}}>
|
||||
{hasError && <div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay">
|
||||
<span
|
||||
className="rsssl-progress-status rsssl-learning-mode-error">{__("Error detected", "really-simple-ssl")}</span>
|
||||
{__("%s cannot be implemented due to server limitations. Check your notices for the detected issue.", "really-simple-ssl").replace('%s', field.label)}
|
||||
<a className="rsssl-learning-mode-link" href="#"
|
||||
onClick={(e) => toggleEnforce(e, false)}>{__("Disable", "really-simple-ssl")}</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{!hasError && <>
|
||||
{enforce != 1 && <button disabled={enforceDisabled} className="button button-primary"
|
||||
onClick={(e) => toggleEnforce(e, true)}>{__("Enforce", "really-simple-ssl")}</button>}
|
||||
{!enforcedByThirdparty && enforce == 1 && <button className="button"
|
||||
onClick={(e) => toggleEnforce(e, false)}>{__("Disable", "really-simple-ssl")}</button>}
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
disabled={enforce}
|
||||
checked={learningMode == 1}
|
||||
value={learningMode}
|
||||
onChange={(e) => toggleLearningMode(e)}
|
||||
/>
|
||||
{__("Enable Learning Mode to configure automatically", "really-simple-ssl")}
|
||||
</label>
|
||||
{enforce == 1 && <div className="rsssl-locked">
|
||||
<div className="rsssl-shield-overlay">
|
||||
<Icon name="shield" size="80px"/>
|
||||
</div>
|
||||
<div className="rsssl-locked-overlay">
|
||||
<span
|
||||
className="rsssl-progress-status rsssl-learning-mode-enforced">{__("Enforced", "really-simple-ssl")}</span>
|
||||
{enforcedString}
|
||||
<a className="rsssl-learning-mode-link" href="#"
|
||||
onClick={(e) => toggleEnforce(e)}>{__("Disable to configure", "really-simple-ssl")}</a>
|
||||
</div>
|
||||
</div>}
|
||||
{learningMode == 1 && <div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay">
|
||||
<span
|
||||
className="rsssl-progress-status rsssl-learning-mode">{__("Learning Mode", "really-simple-ssl")}</span>
|
||||
{configuringString}
|
||||
<a className="rsssl-learning-mode-link" href="#"
|
||||
onClick={(e) => toggleLearningMode(e)}>{__("Exit", "really-simple-ssl")}</a>
|
||||
</div>
|
||||
</div>}
|
||||
{learningModeCompleted == 1 && <div className="rsssl-locked">
|
||||
<div className="rsssl-locked-overlay">
|
||||
<span
|
||||
className="rsssl-progress-status rsssl-learning-mode-completed">{__("Learning Mode", "really-simple-ssl")}</span>
|
||||
{__("We finished the configuration.", "really-simple-ssl")}
|
||||
<a className="rsssl-learning-mode-link" href="#"
|
||||
onClick={(e) => toggleLearningMode(e)}>{__("Review the settings and enforce the policy", "really-simple-ssl")}</a>
|
||||
</div>
|
||||
</div>}
|
||||
{rsssl_settings.pro_plugin_active && props.disabled && <div className="rsssl-locked ">
|
||||
<div className="rsssl-locked-overlay">
|
||||
{!enforcedByThirdparty && <span
|
||||
className="rsssl-progress-status rsssl-disabled">{__("Disabled", "really-simple-ssl")}</span>}
|
||||
{enforcedByThirdparty && <span
|
||||
className="rsssl-progress-status rsssl-learning-mode-enforced">{__("Enforced", "really-simple-ssl")}</span>}
|
||||
{disabledString}
|
||||
</div>
|
||||
</div>}
|
||||
</>
|
||||
}
|
||||
<Filter/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LearningMode
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user