Initial commit: Atomaste website

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

View File

@@ -0,0 +1,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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
import Onboarding from "../Onboarding/Onboarding";
const Activate = () => {
return (
<div className="rsssl-lets-encrypt-tests">
<Onboarding/>
</div>
)
}
export default Activate;

View File

@@ -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")}/>
&nbsp;{__("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;

View File

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

View File

@@ -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")}&nbsp;
<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")}&nbsp;
</p>
<Button
variant="secondary"
onClick={() => handleSkipDNS()}
>
{ __( 'Skip DNS check', 'really-simple-ssl' ) }
</Button>
</>
}
</div>
);
}
export default Generation;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
.rsssl-modal-premium-container {
background-color: var(--rsp-dark-blue);
color:#fff;
padding:0 5px;
margin-right:22px;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,41 @@
.rsssl-modal-body, .rsssl {
input.MuiInput-underline:before {
display:none;
}
.MuiAutocomplete-root {
.MuiInputLabel-outlined[data-shrink=false] {
transform: translate(14px, 16px) scale(1);
}
.MuiFormLabel-root {
font-family:inherit;
}
.MuiOutlinedInput-root {
padding: 0;
}
}
input.MuiAutocomplete-input[type=text] {
&:focus {
outline: none;
box-shadow:none;
}
border:0;
padding-left:12px;
}
.MuiInputBase-root {
font-family: inherit;
}
.MuiInput-root input.MuiInputBase-input {
padding-left:10px;
}
.MuiPopper-root, .MuiPaper-root {
max-height:150px;
z-index: 999999;
div {
font-family: inherit !important;
}
ul {
max-height:initial;
}
}
}

View File

@@ -0,0 +1,62 @@
/*
* The native selectControl doesn't allow disabling per option.
*/
import DOMPurify from "dompurify";
import {Autocomplete} from "@mui/material";
import TextField from '@material-ui/core/TextField';
import './AutoComplete.scss';
import { makeStyles } from "@material-ui/styles";
const useStyles = makeStyles(() => ({
autoComplete: {
fontSize: "12px"
}
}));
const AutoCompleteControl = ({field, disabled, value, options, label, onChange }) => {
let selectDisabled = !Array.isArray(disabled) && disabled;
const classes = useStyles();
return (
<>
<Autocomplete
classes={{
input: classes.autoComplete,
option: classes.autoComplete
}}
disabled={selectDisabled}
disablePortal
value={ value }
id={field.id}
options={options}
isOptionEqualToValue={(option, value) => {
const optionValue = typeof option.value === "string" ? option.value.toLowerCase() : option.value;
const valueValue = typeof value.value === "string" ? value.value.toLowerCase() : value.value;
return optionValue === valueValue;
}}
getOptionLabel={(option) => {
if ( option && option.label ) {
return option.label;
}
const selectedOption = options.find( item => item.value === option );
if ( selectedOption ) {
return selectedOption.label;
}
return option;
} }
onChange={(event, newValue) => {
let value = newValue && newValue.value ? newValue.value : '';
onChange(value);
}}
renderInput={(params) => <TextField {...params}
label={label}
margin="normal"
variant="outlined"
fullWidth
/>}
/>
</>
);
}
export default AutoCompleteControl

View File

@@ -0,0 +1,45 @@
import Hyperlink from "../utils/Hyperlink";
import * as rsssl_api from "../utils/api";
import useFields from "./FieldsData";
import Icon from "../utils/Icon";
import {useState} from "@wordpress/element";
/**
* Render a help notice in the sidebar
*/
const Button = (props) => {
const {addHelpNotice} = useFields();
const [processing, setProcessing] = useState(false);
const {fields} = useFields();
const onClickHandler = async (action) => {
let data = {};
setProcessing(true);
data.fields = fields;
await rsssl_api.doAction(action, data).then((response) => {
let label = response.success ? 'success' : 'warning';
let title = response.title;
let text = response.message;
setProcessing(false);
addHelpNotice(props.field.id, label, text, title, false);
});
}
let is_disabled = !!props.field.disabled;
return (
<>
{ props.field.url &&
<Hyperlink className={"button button-default"} disabled={is_disabled} text={props.field.button_text} url={props.field.url}/>
}
{ props.field.action &&
<button onClick={ () => onClickHandler( props.field.action ) } className="button button-default" disabled={is_disabled}>
{props.field.button_text}
{processing && <Icon name = "loading" color = 'grey' />}
</button>
}
</>
);
}
export default Button

View File

@@ -0,0 +1,89 @@
import ReCaptcha from './ReCaptcha';
import HCaptcha from './HCaptcha';
import useFields from '../FieldsData';
import useCaptchaData from "./CaptchaData";
import {__} from '@wordpress/i18n';
import {useEffect, useState} from "@wordpress/element";
import ErrorBoundary from "../../utils/ErrorBoundary";
import Button from "../Button";
const Captcha = ({props}) => {
const {getFieldValue, updateField, saveFields, getField} = useFields();
const enabled_captcha_provider = getFieldValue('enabled_captcha_provider');
const siteKey = getFieldValue(`${enabled_captcha_provider}_site_key`);
const secretKey = getFieldValue(`${enabled_captcha_provider}_secret_key` );
const fully_enabled = getFieldValue('captcha_fully_enabled');
const {verifyCaptcha, setReloadCaptcha, removeRecaptchaScript} = useCaptchaData();
const [showCaptcha, setShowCaptcha] = useState(false);
const [buttonEnabled, setButtonEnabled] = useState(false);
const handleCaptchaResponse = (response) => {
verifyCaptcha(response).then((response) => {
if (response && response.success) {
updateField('captcha_fully_enabled', 1);
saveFields(false, false, true);
} else {
updateField('captcha_fully_enabled', false);
saveFields(false, false);
}
});
};
//if we switch to another captcha provider, we need to reset the captcha
useEffect(() => {
saveFields(false, false);
}, [enabled_captcha_provider]);
useEffect(() => {
if (fully_enabled) {
updateField('captcha_fully_enabled', 1);
saveFields(false, false);
}
}, [fully_enabled]);
useEffect(() => {
setShowCaptcha(false);
//based on the provider the keys need certain length if hcapthca the length is 36 and recapthca 40
switch (enabled_captcha_provider) {
case 'recaptcha':
if (siteKey.length === 40 && secretKey.length === 40) {
setButtonEnabled(true);
} else {
setButtonEnabled(false);
}
break;
case 'hcaptcha':
if (siteKey.length === 36 && secretKey.length === 35) {
setButtonEnabled(true);
} else {
setButtonEnabled(false);
}
break;
}
}, [siteKey, secretKey, enabled_captcha_provider]);
return (
<div>
<ErrorBoundary title={__('Reload Captcha' , 'really-simple-ssl')}>
{enabled_captcha_provider === 'recaptcha' && !fully_enabled && showCaptcha && (
<ReCaptcha handleCaptchaResponse={handleCaptchaResponse} />
)}
{enabled_captcha_provider === 'hcaptcha' && !fully_enabled && showCaptcha && (
<HCaptcha sitekey={siteKey} handleCaptchaResponse={handleCaptchaResponse} captchaVerified={fully_enabled}/>
)}
{enabled_captcha_provider !== 'none' && !fully_enabled && (
<button
disabled={!buttonEnabled}
className={`button button-primary ${!buttonEnabled ? 'rsssl-learning-mode-disabled' : ''}`}
// style={{display: !showCaptcha? 'none': 'block'}}
onClick={() => setShowCaptcha(true)}> {__('Validate CAPTCHA', 'really-simple-ssl')} </button>)
}
</ErrorBoundary>
</div>
);
};
export default Captcha;

View File

@@ -0,0 +1,40 @@
import {create} from 'zustand';
import * as rsssl_api from "../../utils/api";
const useCaptchaData = create(( set, get ) => ({
reloadCaptcha: false,
setReloadCaptcha: ( value ) => set({ reloadCaptcha: value }),
verifyCaptcha: async ( responseToken ) => {
try {
const response = await rsssl_api.doAction('verify_captcha', { responseToken: responseToken });
// Handle the response
if ( !response ) {
console.error('No response received from the server.');
return;
}
return response;
} catch (error) {
console.error('Error:', error);
}
},
removeRecaptchaScript: async(source = 'recaptcha') => {
if (window.grecaptcha) {
window.grecaptcha.reset();
delete window.grecaptcha;
}
const scriptTags = document.querySelectorAll('script[src^="https://www.google.com/recaptcha/api.js"]');
// For each found script tag
scriptTags.forEach((scriptTag) => {
scriptTag.remove(); // Remove it
});
const rescriptTags = document.querySelectorAll('script[src^="https://www.google.com/recaptcha/api.js"]');
// now we check if reCaptcha was still rendered.
const recaptchaContainer = document.getElementById('recaptchaContainer');
if (recaptchaContainer) {
recaptchaContainer.remove();
}
},
}));
export default useCaptchaData;

View File

@@ -0,0 +1,39 @@
import Icon from "../../utils/Icon";
import useFields from "../FieldsData";
import {TextControl} from "@wordpress/components"; // assuming you're using WordPress components
const CaptchaKey = ({ field, fields, label }) => {
const { getFieldValue, setChangedField, updateField, saveFields} = useFields();
let fieldValue = getFieldValue(field.id);
let captchaVerified = getFieldValue('captcha_fully_enabled');
const onChangeHandler = async (fieldValue) => {
setChangedField(field.id, fieldValue);
setChangedField('captcha_fully_enabled', false);
updateField(field.id, fieldValue);
await saveFields(false, false);
}
return (
<>
<TextControl
required={field.required}
placeholder={field.placeholder}
help={field.comment}
label={label}
onChange={(value) => onChangeHandler(value)}
value={fieldValue}
/>
<div className="rsssl-email-verified" >
{Boolean(captchaVerified)
? <Icon name='circle-check' color={'green'} />
: <Icon name='circle-times' color={'red'} />
}
</div>
</>
);
}
export default CaptchaKey;

View File

@@ -0,0 +1,46 @@
import {useEffect} from "@wordpress/element";
const HCaptcha = ({ sitekey, handleCaptchaResponse }) => {
const hcaptchaCallback = (response) => {
handleCaptchaResponse(response);
};
useEffect(() => {
const script = document.createElement('script');
script.src = `https://hcaptcha.com/1/api.js?onload=initHcaptcha`;
script.async = true;
script.defer = true;
script.onload = () => {
if (typeof window.hcaptcha !== 'undefined') {
window.hcaptcha.render('hcaptchaContainer', {
sitekey: sitekey,
callback: hcaptchaCallback
});
}
};
document.body.appendChild(script);
// Cleanup function
return () => {
// Check if hcaptcha is loaded before trying to remove it
if (window.hcaptcha) {
window.hcaptcha.reset();
}
if (script) {
script.remove();
}
};
}, [sitekey, handleCaptchaResponse]);
return (
<div className="rsssl-captcha"
style={{display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '20px'}}>
<div id='hcaptchaContainer'></div>
</div>
);
};
export default HCaptcha;

View File

@@ -0,0 +1,62 @@
import {useEffect} from "@wordpress/element";
import useFields from '../FieldsData';
import CaptchaData from "./CaptchaData";
/**
* ReCaptcha functionality.
*
* @param {function} handleCaptchaResponse - The callback function to handle the ReCaptcha response.
* @param {boolean} captchaVerified - Boolean value indicating whether the ReCaptcha is verified or not.
* @return {JSX.Element} - The ReCaptcha component JSX.
*/
const ReCaptcha = ({ handleCaptchaResponse , captchaVerified}) => {
const recaptchaCallback = (response) => {
handleCaptchaResponse(response);
};
const {reloadCaptcha, removeRecaptchaScript, setReloadCaptcha} = CaptchaData();
const {getFieldValue, updateField, saveFields} = useFields();
const sitekey = getFieldValue('recaptcha_site_key');
const secret = getFieldValue('recaptcha_secret_key');
const fully_enabled = getFieldValue('captcha_fully_enabled');
useEffect(() => {
const script = document.createElement('script');
script.src = `https://www.google.com/recaptcha/api.js?render=explicit&onload=initRecaptcha`;
script.async = true;
script.defer = true;
script.onload = () => {
// We restore the recaptcha script if it was not removed.
let recaptchaContainer = document.getElementById('recaptchaContainer');
if (typeof window.grecaptcha !== 'undefined') {
window.initRecaptcha = window.initRecaptcha || (() => {
window.grecaptcha && window.grecaptcha.render(recaptchaContainer, {
sitekey: sitekey,
callback: recaptchaCallback,
});
});
}
};
document.body.appendChild(script);
}, [sitekey, handleCaptchaResponse]);
useEffect(() => {
// Move cleanup here.
if (captchaVerified) {
removeRecaptchaScript();
}
}, [captchaVerified]);
return (
<div className="rsssl-captcha"
style={{display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '20px'}} >
<div id='recaptchaContainer'></div>
</div>
);
};
export default ReCaptcha;

View File

@@ -0,0 +1,111 @@
/*
* The tooltip can't be included in the native toggleControl, so we have to build our own.
*/
import { useState, useRef, useEffect } from "@wordpress/element";
import { __experimentalConfirmDialog as ConfirmDialog } from '@wordpress/components';
import hoverTooltip from "../utils/hoverTooltip";
import {__} from '@wordpress/i18n';
const CheckboxControl = (props) => {
const checkboxRef = useRef(null);
let disabledCheckboxPropBoolean = (props.disabled === true);
let disabledCheckboxViaFieldConfig = (props.field.disabled === true);
let checkboxDisabled = (
disabledCheckboxViaFieldConfig
|| disabledCheckboxPropBoolean
);
let tooltipText = '';
let emptyValues = [undefined, null, ''];
if (checkboxDisabled
&& props.field.hasOwnProperty('disabledTooltipHoverText')
&& !emptyValues.includes(props.field.disabledTooltipHoverText)
) {
tooltipText = props.field.disabledTooltipHoverText;
}
hoverTooltip(
checkboxRef,
(checkboxDisabled && (tooltipText !== '')),
tooltipText
);
// const tooltipText = __("404 errors detected on your home page. 404 blocking is unavailable, to prevent blocking of legitimate visitors. It is strongly recommended to resolve these errors.", "really-simple-ssl");
// // Pass props.disabled as the condition
// hoverTooltip(checkboxRef, props.disabled, tooltipText);
const [ isOpen, setIsOpen ] = useState( false );
const onChangeHandler = (e) => {
// WordPress <6.0 does not have the confirmdialog component
if ( !ConfirmDialog ) {
executeAction();
return;
}
if (props.field.warning && props.field.warning.length>0 && !props.field.value) {
setIsOpen( true );
} else {
executeAction();
}
}
const handleConfirm = async () => {
setIsOpen( false );
executeAction();
};
const handleCancel = () => {
setIsOpen( false );
};
const executeAction = (e) => {
let fieldValue = !props.field.value;
props.onChangeHandler(fieldValue)
}
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
onChangeHandler(true);
}
}
let field = props.field;
let is_checked = field.value ? 'is-checked' : '';
let is_disabled = props.disabled ? 'is-disabled' : '';
return (
<>
{ConfirmDialog && <ConfirmDialog
isOpen={ isOpen }
onConfirm={ handleConfirm }
onCancel={ handleCancel }
>
{field.warning}
</ConfirmDialog> }
<div className="components-base-control components-toggle-control">
<div className="components-base-control__field">
<div data-wp-component="HStack" className="components-flex components-h-stack">
<span className={ "components-form-toggle "+is_checked + ' ' +is_disabled}>
<input
ref={checkboxRef}
onKeyDown={(e) => handleKeyDown(e)}
checked={props.value}
className="components-form-toggle__input"
onChange={ ( e ) => onChangeHandler(e) }
id={props.id}
type="checkbox"
disabled={props.disabled}
/>
<span className="components-form-toggle__track"></span>
<span className="components-form-toggle__thumb"></span>
</span>
<label htmlFor={field.id} className="components-toggle-control__label">{props.label}</label>
</div>
</div>
</div>
</>
);
}
export default CheckboxControl

View File

@@ -0,0 +1,26 @@
.rsssl-datatable-component {
.rsssl-action-buttons__inner {
.rsssl-action-buttons__button {
&.rsssl-red {
border: 0 solid transparent;
background: var(--rsp-red);
color: var(--rsp-text-color-white);
&:hover {
background: var(--rsp-dark-red);
color: var(--rsp-text-color-white);
}
}
}
}
.rsssl-add-button__button, .rsssl-action-buttons__button {
//display: flex;
.rsssl-icon {
margin-right: 10px;
}
}
}

View File

@@ -0,0 +1,23 @@
import DataTableStore from "../DataTableStore";
import './Buttons.scss'
import Icon from "../../../utils/Icon";
const ControlButton = ({ controlButton }) => {
const {
processing,
} = DataTableStore();
return (
<div className="rsssl-add-button">
<div className="rsssl-add-button__inner">
<button
className="button button-secondary button-datatable rsssl-add-button__button"
onClick={controlButton.onClick}
disabled={processing}
>
{processing && <Icon name = "loading" color = 'grey' />}
{controlButton.label}
</button>
</div>
</div>
);
};
export default ControlButton;

View File

@@ -0,0 +1,24 @@
import DataTableStore from "../DataTableStore";
import './Buttons.scss'
import Icon from "../../../utils/Icon";
import {memo} from "@wordpress/element";
const MultiSelectButton = ({ids, buttonData}) => {
const {
processing,
rowAction,
} = DataTableStore();
return (
<div className={`rsssl-action-buttons__inner`}>
<button
className={`button ${buttonData.className} rsssl-action-buttons__button`}
onClick={(e) => rowAction(ids, buttonData.action, buttonData.type, buttonData.reloadFields) }
disabled={processing}
>
{processing && <Icon name = "loading" color = 'grey' />}
{buttonData.label}
</button>
</div>
);
};
export default memo(MultiSelectButton)

View File

@@ -0,0 +1,23 @@
import DataTableStore from "../DataTableStore";
import './Buttons.scss'
import Icon from "../../../utils/Icon";
import {memo} from "@wordpress/element";
const RowButton = ({id, buttonData}) => {
const {
processing,
rowAction,
} = DataTableStore();
return (
<div className={`rsssl-action-buttons__inner`}>
<button
className={`button ${buttonData.className} rsssl-action-buttons__button`}
onClick={(e) => rowAction([id], buttonData.action, buttonData.type, buttonData.reloadFields) }
disabled={processing}
>
{buttonData.label}
</button>
</div>
);
};
export default memo(RowButton);

View File

@@ -0,0 +1,16 @@
//style for checkbox when some rows are selected
.rsssl-indeterminate {
input[name="select-all-rows"] {
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="10" y="45" width="80" height="10" fill="currentColor"/></svg>') no-repeat center center;
}
}
//style for checkbox when all rows are selected
.rsssl-all-selected {
input[name="select-all-rows"]::before {
content: url(data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%3Cpath%20d%3D%27M14.83%204.89l1.34.94-5.81%208.38H9.02L5.78%209.67l1.34-1.25%202.57%202.4z%27%20fill%3D%27%233582c4%27%2F%3E%3C%2Fsvg%3E);
margin: -0.1875rem 0 0 -0.25rem;
height: 1.3125rem;
width: 1.3125rem;
}
}

View File

@@ -0,0 +1,55 @@
.rsssl-datatable-component {
margin-left: calc(0px - var(--rsp-spacing-l));
margin-right: calc(0px - var(--rsp-spacing-l));
>div {
//prevent scrollbar on datatable
overflow: hidden;
}
.rdt_TableCol, .rdt_TableCell, .rdt_TableCol_Sortable {
flex-direction: row;
}
.rdt_TableCol:first-child, .rdt_TableCell:first-child {
min-width: initial;
}
.rdt_TableHeadRow {
.rdt_TableCol:last-child {
flex-grow: 0;
flex-direction: row-reverse;
min-width: initial;
}
}
.rdt_TableRow {
&:nth-child(odd) {
background-color: var(--rsp-grey-200)
}
padding: var(--rsp-spacing-xs) 0;
.rdt_TableCell:last-child {
flex-grow: 0;
}
}
//wp-core also adds an svg for the select dropdown, so we hide the one from the react datatables component
nav.rdt_Pagination > div > svg {
display: none !important;
}
.rsssl-container {
padding: 2em;
display: flex;
align-items: center;
justify-content: space-between;
}
}

View File

@@ -0,0 +1,101 @@
import {create} from 'zustand';
import * as rsssl_api from "../../utils/api";
import {produce} from "immer";
const DataTableStore = create((set, get) => ({
processing: false,
dataLoaded: false,
dataActions: {},
sourceData: [],
filteredData: [],
searchTerm:'',
searchColumns:[],
reloadFields:false,
setReloadFields: (reloadFields) => set({reloadFields}),
clearAllData: () => set({sourceData: [], filteredData: []}),
setProcessing: (processing) => set({processing}),
fetchData: async (action, dataActions) => {
set({processing: true});
try {
const response = await rsssl_api.doAction(
action,
dataActions
);
if (response && response.data ) {
set({filteredData:response.data, sourceData: response.data, dataLoaded: true, processing: false});
}
} catch (e) {
console.log(e);
} finally {
set({processing: false});
}
},
handleSearch: (searchTerm, searchColumns) => {
set({searchTerm})
set({searchColumns})
let data = get().sourceData;
const filteredData = data.filter(item =>
searchColumns.some(column =>
item[column] && item[column].toLowerCase().includes(searchTerm.toLowerCase())
));
set({filteredData: filteredData});
},
/*
* This function handles the filter, it is called from the GroupSetting class
*/
handleFilter: async (column, filterValue) => {
//Add the column and sortDirection to the dataActions
set(produce((state) => {
state.dataActions = {...state.dataActions, filterColumn: column, filterValue};
})
);
},
restoreView: () => {
//filter the data again
let searchTerm = get().searchTerm;
if ( searchTerm !== '' ) {
let searchColumns = get().searchColumns;
get().handleSearch(searchTerm, searchColumns);
}
},
//only removes rows from the dataset clientside, does not do an API call
removeRows:(ids) => {
let filteredData = get().filteredData;
let sourceData = get().sourceData;
let newFilteredData = filteredData.filter(item => !ids.includes(item.id));
let newSourceData = sourceData.filter(item => !ids.includes(item.id));
set({filteredData: newFilteredData, sourceData: newSourceData});
get().restoreView();
},
rowAction: async ( ids, action, actionType, reloadFields ) => {
actionType = typeof actionType !== 'undefined' ? actionType : '';
set({processing: true});
if ( actionType === 'delete' ) {
get().removeRows(ids);
}
let data = {
ids: ids,
};
try {
const response = await rsssl_api.doAction(
action,
data
);
if ( response.data ) {
set({filteredData:response.data, sourceData: response.data, dataLoaded: true, processing: false});
get().restoreView();
if (reloadFields) {
get().setReloadFields(reloadFields);
}
}
} catch (e) {
} finally {
set({processing: false});
}
},
}));
export default DataTableStore;

View File

@@ -0,0 +1,176 @@
import {useEffect, useState , memo } from "@wordpress/element";
import DataTable, { createTheme } from "react-data-table-component";
import DataTableStore from "../DataTable/DataTableStore";
import { __ } from '@wordpress/i18n';
import ControlButton from "../DataTable/Buttons/ControlButton";
import RowButton from "../DataTable/Buttons/RowButton";
import SearchBar from "../DataTable/SearchBar/SearchBar";
import SelectedRowsControl from "../DataTable/SelectedRowsControl/SelectedRowsControl";
import './DataTable.scss';
import './Checkboxes.scss';
import useFields from "../FieldsData";
import useMenu from "../../Menu/MenuData";
const DataTableWrapper = ({field, controlButton, enabled}) => {
const {
filteredData,
handleSearch,
dataLoaded,
fetchData,
reloadFields,
} = DataTableStore();
const {fetchFieldsData} = useFields();
const {selectedSubMenuItem} = useMenu();
const [rowsSelected, setRowsSelected] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [rowsPerPage, setRowsPerPage] = useState(10);
useEffect(() => {
if ( !dataLoaded) {
fetchData(field.action, {});
}
}, [dataLoaded] );
useEffect(() => {
if ( reloadFields ) {
fetchFieldsData(selectedSubMenuItem);
}
}, [reloadFields]);
/**
* Build a column configuration object.
*
* @param {object} column - The column object.
* @param {string} column.name - The name of the column.
* @param {boolean} column.sortable - Whether the column is sortable.
* @param {boolean} column.searchable - Whether the column is searchable.
* @param {number} column.width - The width of the column.
* @param {boolean} column.visible - Whether the column is visible.
* @param {string} column.column - The column identifier.
*
* @returns {object} The column configuration object.
*/
const buildColumn = ({reloadFields, name, isButton, action, label, className, sortable, searchable, width, visible, column}) => ({
reloadFields, name, isButton, action, label, className, sortable, searchable, width, visible, column, selector: row => row[column],
});
const columns = field.columns.map(buildColumn);
const buttonColumns = columns.filter(column => column.isButton);
const hasSelectableRows = buttonColumns.length>0;
const searchableColumns = columns.filter(column => column.searchable).map(column => column.column);
const customgitStyles = {
headCells: {
style: {
paddingLeft: '0',
paddingRight: '0',
},
},
cells: {
style: {
paddingLeft: '0',
paddingRight: '0',
},
},
};
createTheme('really-simple-plugins', {
divider: {
default: 'transparent',
},
}, 'light');
const handleSelection = ({selectedCount, selectedRows}) => {
// based on the current page and the rows per page we get the rows that are selected
let actualRows = rowsPerPage;
//in case not all selected, get the rows that are selected from the current page.
//the datatable component selects 'all' rows, but we only want the rows from the current page.
let rows = [];
if ( selectedCount < rowsPerPage ) {
rows = selectedRows;
setRowsSelected(selectedRows);
} else if ( selectedCount >= rowsPerPage ) {
//previously all rows were selected, but now some were unselected.
//in the latter case we need to get the rows that are selected from the current page.
//remove the rows from all pages after the current page
let diff = filteredData.length - selectedRows.length;
rows = selectedRows.slice( 0, (currentPage * rowsPerPage) - diff );
if ( currentPage > 1 ) {
//remove the rows from all pages before the current page from the selected rows
rows = rows.slice( (currentPage - 1) * rowsPerPage);
}
setRowsSelected(rows);
}
}
const data= dataLoaded && filteredData.length>0 ? {...filteredData} : [];
for (const key in data) {
const dataItem = {...data[key]};
//check if there exists a column with column = 'actionButton'
if ( buttonColumns.length > 0 ) {
for (const buttonColumn of buttonColumns) {
dataItem[buttonColumn.column] = <RowButton id={dataItem.id} buttonData={buttonColumn}/>
}
}
data[key] = dataItem;
}
let selectAllRowsClass = "";
if ( rowsSelected.length>0 && rowsSelected.length < rowsPerPage) {
selectAllRowsClass = "rsssl-indeterminate";
}
if ( rowsSelected.length === rowsPerPage ) {
selectAllRowsClass = "rsssl-all-selected";
}
return (
<div className={"rsssl-datatable-component"}>
<div className="rsssl-container">
{controlButton.show && <ControlButton controlButton={controlButton}/> }
{/*Ensure that positioning also works without the addButton, by adding a div */}
{ !controlButton.show && <div></div>}
<SearchBar
handleSearch={handleSearch}
searchableColumns={searchableColumns}
/>
</div>
{ field.multiselect_buttons && rowsSelected.length > 0 && (
<SelectedRowsControl rowsSelected={rowsSelected} buttonData = {field.multiselect_buttons} />
)}
<DataTable
className={ selectAllRowsClass }
columns={columns}
data={Object.values(data)}
dense
pagination={true}
paginationComponentOptions={{
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
rangeSeparatorText: __('of', 'really-simple-ssl'),
noRowsPerPage: false,
selectAllRowsItem: false,
selectAllRowsItemText: __('All', 'really-simple-ssl'),
}}
noDataComponent={__("No results", "really-simple-ssl")}
persistTableHead
selectableRows={hasSelectableRows}
//clearSelectedRows={() => setRowsSelected([])}
paginationPerPage={rowsPerPage}
onChangePage={setCurrentPage}
onChangeRowsPerPage={setRowsPerPage}
onSelectedRowsChange={handleSelection}
theme="really-simple-plugins"
// customStyles={customStyles}
/>
{!enabled && (
<div className="rsssl-locked">
<div className="rsssl-locked-overlay">
<span className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span>
<span>{__('Here you can add IP addresses that should never be blocked by region restrictions.', 'really-simple-ssl')}</span>
</div>
</div>
)}
</div>
);
}
export default memo(DataTableWrapper);

View File

@@ -0,0 +1,31 @@
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import './SearchBar.scss';
import {memo} from "@wordpress/element";
const SearchBar = ({ handleSearch, searchableColumns }) => {
const [debounceTimer, setDebounceTimer] = useState(null);
const onKeyUp = (event) => {
clearTimeout(debounceTimer);
setDebounceTimer(setTimeout(() => {
handleSearch(event.target.value, searchableColumns)
}, 500));
};
return (
<div className="rsssl-search-bar">
<div className="rsssl-search-bar__inner">
<div className="rsssl-search-bar__icon"></div>
<input
type="text"
className="rsssl-search-bar__input"
placeholder={__("Search", "really-simple-ssl")}
onKeyUp={onKeyUp}
/>
</div>
</div>
)
}
export default memo(SearchBar);

View File

@@ -0,0 +1,32 @@
.rsssl-search-bar {
float: right;
padding: 0;
}
.rsssl-search-bar__inner {
display: flex;
align-items: center;
border-radius: 3px;
transition: background-color 0.3s ease;
}
.rsssl-search-bar__inner:focus-within {
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.rsssl-search-bar__icon {
/* Add styles for the search icon */
}
.rsssl-search-bar__input {
border: none;
outline: none;
padding: 3px 5px;
width: 150px; /* Adjust width as needed */
transition: width 0.3s ease;
}
.rsssl-search-bar__input:focus {
width: 200px; /* Adjust width as needed */
}

View File

@@ -0,0 +1,41 @@
import {__, _n} from "@wordpress/i18n";
import DataTableStore from "../DataTableStore";
import MultiSelectButton from "../Buttons/MultiSelectButton";
import './SelectedRowsControl.scss'
import {memo} from "@wordpress/element";
import MenuItem from "../../../Menu/MenuItem";
const SelectedRowsControl = ({ rowsSelected, buttonData }) => {
const {
processing,
filteredData,
} = DataTableStore();
//ensure that all items in the rowsSelected array still exist in the filteredData array
//after a delete this might not be the case
let rowsSelectedFiltered = rowsSelected.filter(selectedRow =>
filteredData.some(filteredRow => filteredRow.id === selectedRow.id)
);
if ( rowsSelectedFiltered.length === 0 ) {
return null;
}
//parse ids from rowsSelected into array
const ids = rowsSelectedFiltered.map((row) => row.id);
return (
<div className="rsssl-selected-rows-control">
<div className={"rsssl-multiselect-datatable-form rsssl-primary"}>
<div>
{_n( "You have selected %d row", "You have selected %d rows", rowsSelectedFiltered.length, 'really-simple-ssl' ).replace('%d', rowsSelectedFiltered.length )}
</div>
<div className="rsssl-action-buttons">
<>
{ buttonData.map((buttonItem, i) => <MultiSelectButton key={"multiselectButton-"+i} ids={ids} buttonData={buttonItem} /> ) }
</>
</div>
</div>
</div>
)
}
export default memo(SelectedRowsControl);

View File

@@ -0,0 +1,14 @@
.rsssl-selected-rows-control {
margin-top: 1em;
margin-bottom: 1em;
//blue container above datatable for multiselect
.rsssl-multiselect-datatable-form {
display: flex;
align-items: center;
Justify-content: space-between;
width: 100%;
padding: 1em 2em;
background: var(--rsp-blue-faded);
}
}

View File

@@ -0,0 +1,21 @@
const AddButton = ({ getCurrentFilter, moduleName, handleOpen, processing, blockedText, allowedText }) => {
let buttonText = getCurrentFilter(moduleName) === 'blocked' ? blockedText : allowedText;
return (
<div className="rsssl-add-button">
{(getCurrentFilter(moduleName) === 'blocked' || getCurrentFilter(moduleName) === 'allowed') && (
<div className="rsssl-add-button__inner">
<button
className="button button-secondary button-datatable rsssl-add-button__button"
onClick={handleOpen}
disabled={processing}
>
{buttonText}
</button>
</div>
)}
</div>
);
};
export default AddButton;

View File

@@ -0,0 +1,178 @@
import {__} from '@wordpress/i18n';
import {useRef, useEffect, useState} from '@wordpress/element';
import DataTable, {createTheme} from "react-data-table-component";
import useFields from "../FieldsData";
import DynamicDataTableStore from "./DynamicDataTableStore";
const DynamicDataTable = (props) => {
const {
twoFAMethods,
setTwoFAMethods,
DynamicDataTable,
dataLoaded,
pagination,
dataActions,
handleTableRowsChange,
fetchDynamicData,
// setDynamicData,
handleTableSort,
handleTablePageChange,
handleTableSearch,
} = DynamicDataTableStore();
let field = props.field;
const [enabled, setEnabled] = useState(false);
const {fields, getFieldValue, saveFields} = useFields();
const twoFAEnabledRef = useRef();
useEffect(() => {
twoFAEnabledRef.current = getFieldValue('login_protection_enabled');
saveFields(true, false)
}, [getFieldValue('login_protection_enabled')]);
useEffect(() => {
const value = getFieldValue('login_protection_enabled');
setEnabled(value);
}, [fields]);
useEffect(() => {
if (!dataLoaded || enabled !== getFieldValue('login_protection_enabled')) {
fetchDynamicData(field.action)
.then(response => {
// Check if response.data is defined and is an array before calling reduce
if(response.data && Array.isArray(response.data)) {
const methods = response.data.reduce((acc, user) => ({...acc, [user.id]: user.rsssl_two_fa_status}), {});
setTwoFAMethods(methods);
} else {
console.error('Unexpected response:', response);
}
})
.catch(err => {
console.error(err); // Log any errors
});
}
}, [dataLoaded, field.action, fetchDynamicData, getFieldValue('login_protection_enabled')]); // Add getFieldValue('login_protection_enabled') as a dependency
useEffect(() => {
if (dataActions) {
fetchDynamicData(field.action);
}
}, [dataActions]);
function buildColumn(column) {
let newColumn = {
name: column.name,
column: column.column,
sortable: column.sortable,
searchable: column.searchable,
width: column.width,
visible: column.visible,
selector: row => row[column.column],
};
return newColumn;
}
let columns = [];
field.columns.forEach(function (item, i) {
let newItem = { ...item, key: item.column };
newItem = buildColumn(newItem);
newItem.visible = newItem.visible ?? true;
columns.push(newItem);
});
let searchableColumns = columns
.filter(column => column.searchable)
.map(column => column.column);
const customStyles = {
headCells: {
style: {
paddingLeft: '0',
paddingRight: '0',
},
},
cells: {
style: {
paddingLeft: '0',
paddingRight: '0',
},
},
};
createTheme('really-simple-plugins', {
divider: {
default: 'transparent',
},
}, 'light');
return (
<>
<div className="rsssl-search-bar">
<div className="rsssl-search-bar__inner">
<div className="rsssl-search-bar__icon"></div>
<input
type="text"
className="rsssl-search-bar__input"
placeholder={__("Search", "really-simple-ssl")}
onChange={event => handleTableSearch(event.target.value, searchableColumns)}
/>
</div>
</div>
{dataLoaded ?
<DataTable
columns={columns}
data={DynamicDataTable}
dense
pagination
paginationServer
onChangeRowsPerPage={handleTableRowsChange}
onChangePage={handleTablePageChange}
sortServer
onSort={handleTableSort}
paginationRowsPerPageOptions={[10, 25, 50, 100]}
noDataComponent={__("No results", "really-simple-ssl")}
persistTableHead
theme="really-simple-plugins"
customStyles={customStyles}
></DataTable>
:
<div className="rsssl-spinner" style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginTop: "100px"
}}>
<div className="rsssl-spinner__inner">
<div className="rsssl-spinner__icon" style={{
border: '8px solid white',
borderTop: '8px solid #f4bf3e',
borderRadius: '50%',
width: '120px',
height: '120px',
animation: 'spin 2s linear infinite'
}}></div>
<div className="rsssl-spinner__text" style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}>{__("Loading data, please stand by...", "really-simple-ssl")}</div>
</div>
</div>
}
{ !enabled &&
<div className="rsssl-locked">
<div className="rsssl-locked-overlay"><span
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Activate Two-Factor Authentication to enable this block.', 'really-simple-ssl')}</span>
</div>
</div>
}
</>
);
}
export default DynamicDataTable;

View File

@@ -0,0 +1,77 @@
/* Creates A Store For Risk Data using Zustand */
import {create} from 'zustand';
import * as rsssl_api from "../../utils/api";
import {produce} from "immer";
import React, {useState} from "react";
const DynamicDataTableStore = create((set, get) => ({
twoFAMethods: {},
setTwoFAMethods: (methods) => set((state) => ({ ...state, twoFAMethods: methods })),
processing: false,
dataLoaded: false,
pagination: {},
dataActions: {},
DynamicDataTable: [],
fetchDynamicData: async (action) => {
try {
const response = await rsssl_api.doAction(
action,
get().dataActions
);
let data = Array.isArray(response.data) ? response.data : [];
let pagination = response.pagination ? response.pagination : 1;
//now we set the EventLog
if ( response ) {
set(state => ({
...state,
DynamicDataTable: data,
dataLoaded: true,
processing: false,
pagination: pagination,
// Removed the twoFAMethods set from here...
}));
// Return the response for the calling function to use
return response;
}
} catch (e) {
console.log(e);
}
},
handleTableSearch: async (search, searchColumns) => {
set(produce((state) => {
state.dataActions = {...state.dataActions, search, searchColumns};
}));
},
handleTablePageChange: async (page, pageSize) => {
//Add the page and pageSize to the dataActions
set(produce((state) => {
state.dataActions = {...state.dataActions, page, pageSize};
})
);
},
handleTableRowsChange: async (currentRowsPerPage, currentPage) => {
//Add the page and pageSize to the dataActions
set(produce((state) => {
state.dataActions = {...state.dataActions, currentRowsPerPage, currentPage};
})
);
},
//this handles all pagination and sorting
handleTableSort: async (column, sortDirection) => {
//Add the column and sortDirection to the dataActions
set(produce((state) => {
state.dataActions = {...state.dataActions, sortColumn: column, sortDirection};
})
);
},
}));
export default DynamicDataTableStore;

View File

@@ -0,0 +1,29 @@
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
const SearchBar = ({ handleSearch, searchableColumns }) => {
const [debounceTimer, setDebounceTimer] = useState(null);
const onKeyUp = (event) => {
clearTimeout(debounceTimer);
setDebounceTimer(setTimeout(() => {
handleSearch(event.target.value, searchableColumns)
}, 500));
};
return (
<div className="rsssl-search-bar">
<div className="rsssl-search-bar__inner">
<div className="rsssl-search-bar__icon"></div>
<input
type="text"
className="rsssl-search-bar__input"
placeholder={__("Search", "really-simple-ssl")}
onKeyUp={onKeyUp}
/>
</div>
</div>
)
}
export default SearchBar;

View File

@@ -0,0 +1,253 @@
import {__} from '@wordpress/i18n';
import {useEffect, useState} from '@wordpress/element';
import DataTable, {createTheme} from "react-data-table-component";
import EventLogDataTableStore from "./EventLogDataTableStore";
import FilterData from "../FilterData";
import * as rsssl_api from "../../utils/api";
import useMenu from "../../Menu/MenuData";
import Flag from "../../utils/Flag/Flag";
import Icon from "../../utils/Icon";
import useFields from "../FieldsData";
import SearchBar from "../DynamicDataTable/SearchBar";
const EventLogDataTable = (props) => {
const {
DynamicDataTable,
dataLoaded,
pagination,
dataActions,
handleEventTableRowsChange,
fetchDynamicData,
handleEventTableSort,
handleEventTablePageChange,
handleEventTableSearch,
handleEventTableFilter,
processing,
rowCleared,
} = EventLogDataTableStore()
//here we set the selectedFilter from the Settings group
const {
selectedFilter,
setSelectedFilter,
activeGroupId,
getCurrentFilter,
setProcessingFilter,
} = FilterData();
const {fields, fieldAlreadyEnabled, getFieldValue} = useFields();
const [tableHeight, setTableHeight] = useState(600); // Starting height
const rowHeight = 50; // Height of each row.
const moduleName = 'rsssl-group-filter-' + props.field.id;
let field = props.field;
useEffect(() => {
const currentFilter = getCurrentFilter(moduleName);
if (!currentFilter) {
setSelectedFilter('all', moduleName);
}
setProcessingFilter(processing);
handleEventTableFilter('severity', currentFilter);
}, [moduleName, handleEventTableFilter, getCurrentFilter(moduleName), setSelectedFilter, moduleName, DynamicDataTable, processing]);
//if the dataActions are changed, we fetch the data
useEffect(() => {
//we make sure the dataActions are changed in the store before we fetch the data
if (dataActions) {
fetchDynamicData(field.action, field.event_type, dataActions)
}
}, [dataActions.sortDirection, dataActions.filterValue, dataActions.search, dataActions.page, dataActions.currentRowsPerPage]);
//we create the columns
let columns = [];
//getting the fields from the props
//we loop through the fields
field.columns.forEach(function (item, i) {
let newItem = buildColumn(item)
columns.push(newItem);
});
let enabled = fieldAlreadyEnabled('event_log_enabled');
const customStyles = {
headCells: {
style: {
paddingLeft: '0', // override the cell padding for head cells
paddingRight: '0',
},
},
cells: {
style: {
paddingLeft: '0', // override the cell padding for data cells
paddingRight: '0',
},
},
};
createTheme('really-simple-plugins', {
divider: {
default: 'transparent',
},
}, 'light');
//only show the datatable if the data is loaded
if (!dataLoaded && columns.length === 0 && DynamicDataTable.length === 0) {
return (
<div className="rsssl-spinner">
<div className="rsssl-spinner__inner">
<div className="rsssl-spinner__icon"></div>
<div className="rsssl-spinner__text">{__("Loading...", "really-simple-ssl")}</div>
</div>
</div>
);
}
let searchableColumns = [];
//setting the searchable columns
columns.map(column => {
if (column.searchable) {
searchableColumns.push(column.column);
}
});
let data = [];
if (DynamicDataTable.data) {
data = DynamicDataTable.data.map((dataItem) => {
let newItem = {...dataItem};
newItem.iso2_code = generateFlag(newItem.iso2_code, newItem.country_name);
newItem.expandableRows = true;
return newItem;
});
}
//we generate an expandable row
const ExpandableRow = ({data}) => {
let code, icon, color = '';
switch (data.severity) {
case 'warning':
code = 'rsssl-warning';
icon = 'circle-times';
color = 'red';
break;
case 'informational':
code = 'rsssl-primary';
icon = 'info';
color = 'black';
break;
default:
code = 'rsssl-primary';
}
return (
<div className={"rsssl-wizard-help-notice " + code}
style={{padding: '1em', borderRadius: '5px'}}>
{/*now we place a block to the rightcorner with the severity*/}
<div style={{float: 'right'}}>
<Icon name={icon} color={color}/>
</div>
<div style={{fontSize: '1em', fontWeight: 'bold'}}>
{data.severity.charAt(0).toUpperCase() + data.severity.slice(1)}
</div>
<div>{data.description}</div>
</div>
);
};
function generateFlag(flag, title) {
return (
<>
<Flag
countryCode={flag}
style={{
fontSize: '2em',
marginLeft: '0.3em',
}}
title={title}
></Flag>
</>
)
}
let paginationSet;
paginationSet = typeof pagination !== 'undefined';
useEffect(() => {
if (Object.keys(data).length === 0 ) {
setTableHeight(100); // Adjust depending on your UI measurements
} else {
setTableHeight(rowHeight * (paginationSet ? pagination.perPage + 2 : 12)); // Adjust depending on your UI measurements
}
}, [paginationSet, pagination?.perPage, data]);
return (
<>
<div className="rsssl-container">
<div></div>
{/*Display the search bar*/}
<SearchBar handleSearch={handleEventTableSearch} searchableColumns={searchableColumns}/>
</div>
{/*Display the datatable*/}
<DataTable
columns={columns}
data={processing? [] : data}
dense
pagination={!processing}
paginationServer
paginationTotalRows={paginationSet? pagination.totalRows: 10}
paginationPerPage={paginationSet? pagination.perPage: 10}
paginationDefaultPage={paginationSet?pagination.currentPage: 1}
paginationComponentOptions={{
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
rangeSeparatorText: __('of', 'really-simple-ssl'),
noRowsPerPage: false,
selectAllRowsItem: false,
selectAllRowsItemText: __('All', 'really-simple-ssl'),
}}
onChangeRowsPerPage={handleEventTableRowsChange}
onChangePage={handleEventTablePageChange}
expandableRows={!processing}
expandableRowsComponent={ExpandableRow}
loading={dataLoaded}
onSort={handleEventTableSort}
sortServer={!processing}
paginationRowsPerPageOptions={[5, 10, 25, 50, 100]}
noDataComponent={__("No results", "really-simple-ssl")}
persistTableHead
theme="really-simple-plugins"
customStyles={customStyles}
></DataTable>
{!enabled && (
<div className="rsssl-locked">
<div className="rsssl-locked-overlay"><span
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Activate Limit login attempts to enable this block.', 'really-simple-ssl')}</span>
</div>
</div>
)}
</>
);
}
export default EventLogDataTable;
function buildColumn(column) {
return {
name: column.name,
sortable: column.sortable,
searchable: column.searchable,
width: column.width,
visible: column.visible,
column: column.column,
selector: row => row[column.column],
};
}

View File

@@ -0,0 +1,87 @@
/* Creates A Store For Risk Data using Zustand */
import {create} from 'zustand';
import * as rsssl_api from "../../utils/api";
import {__} from "@wordpress/i18n";
import {produce} from "immer";
import React from "react";
const EventLogDataTableStore = create((set, get) => ({
processing: false,
dataLoaded: false,
pagination: {},
dataActions: {},
DynamicDataTable: [],
sorting: [],
rowCleared: false,
fetchDynamicData: async (action, event_type, dataActions = {}) => {
//cool we can fetch the data so first we set the processing to true
set({processing: true});
set({dataLoaded: false});
set({rowCleared: true});
if (Object.keys(dataActions).length === 0) {
dataActions = get().dataActions;
}
// add the data_type to the dataActions
dataActions = {...dataActions, event_type};
//now we fetch the data
try {
const response = await rsssl_api.doAction(
action,
dataActions
);
// now we set the EventLog
if (response) {
set({DynamicDataTable: response, dataLoaded: true, processing: false, pagination: response.pagination, sorting: response.sorting});
}
} catch (e) {
console.log(e);
} finally {
set({processing: false});
set({rowCleared: false});
}
},
handleEventTableSearch: async (search, searchColumns) => {
//Add the search to the dataActions
set(produce((state) => {
state.dataActions = {...state.dataActions, search, searchColumns};
})
);
},
handleEventTablePageChange: async (page, pageSize) => {
//Add the page and pageSize to the dataActions
set(produce((state) => {
state.dataActions = {...state.dataActions, page, pageSize};
})
);
},
handleEventTableRowsChange: async (currentRowsPerPage, currentPage) => {
//Add the page and pageSize to the dataActions
set(produce((state) => {
state.dataActions = {...state.dataActions, currentRowsPerPage, currentPage};
})
);
},
//this handles all pagination and sorting
handleEventTableSort: async (column, sortDirection) => {
set(produce((state) => {
state.dataActions = {...state.dataActions, sortColumn: column, sortDirection};
})
);
},
handleEventTableFilter: async (column, filterValue) => {
//Add the column and sortDirection to the dataActions
set(produce((state) => {
state.dataActions = {...state.dataActions, filterColumn: column, filterValue};
})
);
},
}));
export default EventLogDataTableStore;

View File

@@ -0,0 +1,625 @@
import {
TextControl,
RadioControl,
TextareaControl,
__experimentalNumberControl as NumberControl
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import License from "./License/License";
import Password from "./Password";
import SelectControl from "./SelectControl";
import Host from "./Host/Host";
import Hyperlink from "../utils/Hyperlink";
import LetsEncrypt from "../LetsEncrypt/LetsEncrypt";
import Activate from "../LetsEncrypt/Activate";
import MixedContentScan from "./MixedContentScan/MixedContentScan";
import PermissionsPolicy from "./PermissionsPolicy";
import CheckboxControl from "./CheckboxControl";
import Support from "./Support";
import LearningMode from "./LearningMode/LearningMode";
import RiskComponent from "./RiskConfiguration/RiskComponent";
import VulnerabilitiesOverview from "./RiskConfiguration/vulnerabilitiesOverview";
import IpAddressDatatable from "./LimitLoginAttempts/IpAddressDatatable";
import TwoFaRolesDropDown from "./TwoFA/TwoFaRolesDropDown";
import Button from "./Button";
import Icon from "../utils/Icon";
import { useEffect, useState, useRef } from "@wordpress/element";
import useFields from "./FieldsData";
import PostDropdown from "./PostDropDown";
import NotificationTester from "./RiskConfiguration/NotificationTester";
import getAnchor from "../utils/getAnchor";
import useMenu from "../Menu/MenuData";
import UserDatatable from "./LimitLoginAttempts/UserDatatable";
import CountryDatatable from "./LimitLoginAttempts/CountryDatatable";
import BlockListDatatable from "./GeoBlockList/BlockListDatatable";
import TwoFaDataTable from "./TwoFA/TwoFaDataTable";
import EventLogDataTable from "./EventLog/EventLogDataTable";
import DOMPurify from "dompurify";
import RolesDropDown from "./RolesDropDown";
import GeoDatatable from "./GeoBlockList/GeoDatatable";
import WhiteListDatatable from "./GeoBlockList/WhiteListDatatable";
import Captcha from "./Captcha/Captcha";
import CaptchaKey from "./Captcha/CaptchaKey";
import FileChangeDetection from "./FileChangeDetection/FileChangeDetection";
import UserAgentTable from "./firewall/UserAgentTable";
import TwoFaEnabledDropDown from "./TwoFA/TwoFaEnabledDropDown";
const Field = (props) => {
const scrollAnchor = useRef(null);
const { updateField, setChangedField, highLightField, setHighLightField , getFieldValue} = useFields();
const [anchor, setAnchor] = useState(null);
const { selectedFilter, setSelectedFilter } = useMenu();
useEffect(() => {
// Check URL for anchor and highlightfield parameters
const anchor = getAnchor('anchor');
const highlightField = getAnchor('highlightfield');
setAnchor(anchor);
if (highlightField) {
setHighLightField(highlightField);
}
if (highlightField === props.field.id && scrollAnchor.current) {
scrollAnchor.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
//if the field is a captcha provider, scroll to the captcha provider is a temp fix cause i can't get the scroll to work properly.
if (highLightField === 'enabled_captcha_provider' && props.fields) {
let captchaField = document.getElementsByClassName('rsssl-highlight')[0];
if (captchaField) {
captchaField.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
if (anchor && anchor === props.field.id) {
scrollAnchor.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, []);
useEffect(() => {
handleAnchor();
}, [anchor]);
window.addEventListener('hashchange', () => {
const anchor = getAnchor('anchor');
const highlightField = getAnchor('highlightfield');
setAnchor(anchor);
if (highlightField) {
setHighLightField(highlightField);
}
if (highlightField === props.field.id && scrollAnchor.current) {
scrollAnchor.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
if (anchor && anchor === props.field.id) {
scrollAnchor.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
const handleAnchor = () => {
if (anchor && anchor === props.field.id) {
scrollAnchor.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
const onChangeHandler = (fieldValue) => {
let field = props.field;
if (field.pattern) {
const regex = new RegExp(field.pattern, 'g');
const allowedCharactersArray = fieldValue.match(regex);
fieldValue = allowedCharactersArray ? allowedCharactersArray.join('') : '';
}
updateField(field.id, fieldValue);
// We can configure other fields if a field is enabled, or set to a certain value.
let configureFieldCondition = false;
if (field.configure_on_activation) {
if (field.configure_on_activation.hasOwnProperty('condition') && props.field.value == field.configure_on_activation.condition) {
configureFieldCondition = true;
}
let configureField = field.configure_on_activation[0];
for (let fieldId in configureField) {
if (configureFieldCondition && configureField.hasOwnProperty(fieldId)) {
updateField(fieldId, configureField[fieldId]);
}
}
}
setChangedField(field.id, fieldValue);
};
const labelWrap = (field) => {
let tooltipColor = field.warning ? 'red' : 'black';
return (
<>
<div className="cmplz-label-text">{field.label}</div>
{field.tooltip && <Icon name="info-open" tooltip={field.tooltip} color={tooltipColor} />}
</>
);
};
let field = props.field;
let fieldValue = field.value;
let disabled = field.disabled;
let highLightClass = 'rsssl-field-wrap';
if (highLightField === props.field.id) {
highLightClass = 'rsssl-field-wrap rsssl-highlight';
}
let options = [];
if (field.options) {
for (let key in field.options) {
if (field.options.hasOwnProperty(key)) {
let item = {};
item.label = field.options[key];
item.value = key;
options.push(item);
}
}
}
// If a feature can only be used on networkwide or single site setups, pass that info here.
if (!rsssl_settings.networkwide_active && field.networkwide_required) {
disabled = true;
field.comment = (
<>
{__("This feature is only available networkwide.", "really-simple-ssl")}
<Hyperlink target="_blank" rel="noopener noreferrer" text={__("Network settings", "really-simple-ssl")} url={rsssl_settings.network_link} />
</>
);
}
if (field.conditionallyDisabled) {
disabled = true;
}
if (!field.visible) {
return null;
}
if ( field.type==='checkbox' ) {
return (
<div className={highLightClass} ref={scrollAnchor}>
<CheckboxControl
label={labelWrap(field)}
field={field}
disabled={disabled}
onChangeHandler={ ( fieldValue ) => onChangeHandler( fieldValue ) }
/>
{ field.comment &&
<div className="rsssl-comment" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(field.comment) }} />
/* nosemgrep: react-dangerouslysetinnerhtml */
}
</div>
);
}
if ( field.type==='hidden' ){
return (
<input type="hidden" value={field.value}/>
);
}
if ( field.type==='radio' ){
return (
<div className={highLightClass} ref={scrollAnchor}>
<RadioControl
label={labelWrap(field)}
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
selected={ fieldValue }
options={ options }
/>
</div>
);
}
if (field.type==='email'){
const sendVerificationEmailField = props.fields.find(field => field.id === 'send_verification_email');
const emailIsVerified = sendVerificationEmailField && (sendVerificationEmailField.disabled === false);
return (
<div className={highLightClass} ref={scrollAnchor} style={{position: 'relative'}}>
<TextControl
required={ field.required }
placeholder={ field.placeholder }
disabled={ disabled }
help={ field.comment }
label={labelWrap(field)}
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
value= { fieldValue }
/>
{ sendVerificationEmailField &&
<div className="rsssl-email-verified" >
{!emailIsVerified
? <Icon name='circle-check' color={'green'} />
: <Icon name='circle-times' color={'red'} />}
</div>
}
</div>
);
}
if (field.type==='captcha_key') {
return (
<div className={highLightClass} ref={scrollAnchor} style={{position: 'relative'}}>
<CaptchaKey field={field} fields={props.fields} label={labelWrap(field)} />
</div>
)
}
if ( field.type==='number' ){
return (
<div className={highLightClass} ref={scrollAnchor} style={{ position: 'relative'}}>
<NumberControl
required={ field.required }
placeholder={ field.placeholder }
className="number_full"
disabled={ disabled }
help={ field.comment }
label={labelWrap(field)}
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
value= { fieldValue }
/>
</div>
);
}
if (field.type==='text' ){
return (
<div className={highLightClass} ref={scrollAnchor} style={{position: 'relative'}}>
<TextControl
required={ field.required }
placeholder={ field.placeholder }
disabled={ disabled }
help={ field.comment }
label={labelWrap(field)}
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
value= { fieldValue }
/>
</div>
);
}
if ( field.type==='button' ){
return (
<div className={'rsssl-field-button ' + highLightClass} ref={scrollAnchor}>
<label>{field.label}</label>
<Button field={field}/>
</div>
);
}
if ( field.type==='password' ){
return (
<div className={ highLightClass} ref={scrollAnchor}>
<Password
index={ props.index }
field={ field }
/>
</div>
);
}
if ( field.type==='textarea' ) {
// Handle csp_frame_ancestors_urls differently. Disable on select change
let fieldDisabled = false
if ( field.id === 'csp_frame_ancestors_urls') {
if ( getFieldValue('csp_frame_ancestors') === 'disabled' ) {
fieldDisabled = true
}
} else {
fieldDisabled = field.disabled
}
return (
<div className={highLightClass} ref={scrollAnchor}>
<TextareaControl
label={ field.label }
help={ field.comment }
value= { fieldValue }
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
disabled={ fieldDisabled }
/>
</div>
);
}
if ( field.type==='license' ){
let field = props.field;
let fieldValue = field.value;
return (
<div className={highLightClass} ref={scrollAnchor}>
<License index={props.index} field={field} fieldValue={fieldValue}/>
</div>
);
}
if ( field.type==='number' ){
return (
<div className={highLightClass} ref={scrollAnchor}>
<NumberControl
onChange={ ( fieldValue ) => onChangeHandler(fieldValue) }
help={ field.comment }
label={ field.label }
value= { fieldValue }
/>
</div>
);
}
if ( field.type==='email' ){
return (
<div className={this.highLightClass} ref={this.scrollAnchor}>
<TextControl
help={ field.comment }
label={ field.label }
onChange={ ( fieldValue ) => this.onChangeHandler(fieldValue) }
value= { fieldValue }
/>
</div>
);
}
if ( field.type==='host') {
return (
<div className={highLightClass} ref={scrollAnchor}>
<Host
index={props.index}
field={props.field}
/>
</div>
)
}
if ( field.type==='select') {
return (
<div className={highLightClass} ref={scrollAnchor}>
<SelectControl
disabled={ disabled }
label={labelWrap(field)}
onChangeHandler={ ( fieldValue ) => onChangeHandler(fieldValue) }
value= { fieldValue }
options={ options }
field={field}
/>
</div>
)
}
if ( field.type==='support' ) {
return (
<div className={highLightClass} ref={scrollAnchor}>
<Support/>
</div>
)
}
if ( field.type==='postdropdown' ) {
return (
<div className={highLightClass} ref={scrollAnchor}>
<PostDropdown field={props.field}/>
</div>
)
}
if ( field.type==='permissionspolicy' ) {
return (
<div className={highLightClass} ref={scrollAnchor}>
<PermissionsPolicy disabled={disabled} field={props.field} options={options}/>
</div>
)
}
if (field.type==='captcha') {
return (
<div className={highLightClass} ref={scrollAnchor}>
<Captcha field={field} label={labelWrap(field)} />
</div>
)
}
if ( field.type==='learningmode' ) {
return(
<div className={highLightClass} ref={scrollAnchor}>
<LearningMode disabled={disabled} field={props.field}/>
</div>
)
}
if ( field.type==='riskcomponent' ) {
return (<div className={highLightClass} ref={scrollAnchor}>
<RiskComponent field={props.field}/>
</div>)
}
if ( field.type === 'mixedcontentscan' ) {
return (
<div className={highLightClass} ref={scrollAnchor}>
<MixedContentScan field={props.field}/>
</div>
)
}
if (field.type === 'vulnerabilitiestable') {
return (
<div className={highLightClass} ref={scrollAnchor}>
<VulnerabilitiesOverview field={props.field} />
</div>
)
}
if (field.type === 'two_fa_roles') {
return (
<div className={highLightClass} ref={scrollAnchor}>
<label htmlFor={`rsssl-two-fa-dropdown-${field.id}`}>
{labelWrap(field)}
</label>
<TwoFaRolesDropDown field={props.field} forcedRoledId={props.field.forced_roles_id} optionalRolesId={props.field.optional_roles_id}
/>
</div>
);
}
if (field.type === 'eventlog-datatable') {
return (
<div className={highLightClass} ref={scrollAnchor}>
<EventLogDataTable
field={props.field}
action={props.field.action}
/>
</div>
)
}
if (field.type === 'twofa-datatable') {
return (
<div className={highLightClass} ref={scrollAnchor}>
<TwoFaDataTable
field={props.field}
action={props.field.action}
/>
</div>
)
}
if (field.type === 'ip-address-datatable') {
return (
<div className={highLightClass} ref={scrollAnchor}>
<IpAddressDatatable
field={props.field}
action={props.field.action}
/>
</div>
)
}
if (field.type === 'user-datatable') {
return (
<div className={highLightClass} ref={scrollAnchor}>
<UserDatatable
field={props.field}
action={props.field.action}
/>
</div>
)
}
if (field.type === 'file-change-detection') {
return (
<div className={highLightClass} ref={scrollAnchor}>
<FileChangeDetection
field={props.field}
/>
</div>
)
}
if (field.type === 'country-datatable') {
return (
<div className={highLightClass} ref={scrollAnchor}>
<CountryDatatable
field={props.field}
action={props.field.action}
/>
</div>
)
}
if (field.type === 'geo-datatable') {
return (
<div className={highLightClass} ref={scrollAnchor}>
<GeoDatatable
field={props.field}
action={props.field.action}
/>
</div>
)
}
if (field.type === 'geo-ip-datatable') {
return (
<div className={highLightClass} ref={scrollAnchor}>
<WhiteListDatatable
field={props.field}
action={props.field.action}
/>
</div>
)
}
if (field.type === 'blocklist-datatable') {
return (
<div className={highLightClass} ref={scrollAnchor}>
<BlockListDatatable
field={props.field}
action={props.field.action}
/>
</div>
)
}
if (field.type === 'user-agents-datatable') {
return (
<div className={highLightClass} ref={scrollAnchor}>
<UserAgentTable
field={props.field}
action={props.field.action}
/>
</div>
)
}
if (field.type === 'roles_dropdown') {
return (
<div className={highLightClass} ref={scrollAnchor}>
<label htmlFor={`rsssl-two-fa-dropdown-${field.id}`}>
{labelWrap(field)}
</label>
<RolesDropDown field={props.field}
/>
</div>
);
}
if (field.type === 'roles_enabled_dropdown') {
return (
<div className={highLightClass} ref={scrollAnchor}>
<label htmlFor={`rsssl-two-fa-dropdown-${field.id}`}>
{labelWrap(field)}
</label>
<TwoFaEnabledDropDown field={props.field} disabled={disabled}
/>
</div>
);
}
if(field.type === 'notificationtester') {
return (
<div className={'rsssl-field-button ' + highLightClass} ref={scrollAnchor}>
<NotificationTester field={props.field} disabled={disabled} labelWrap={labelWrap}/>
</div>
)
}
if ( field.type === 'letsencrypt' ) {
return (
<LetsEncrypt field={field} />
)
}
if ( field.type === 'activate' ) {
return (
<Activate field={field}/>
)
}
return (
'not found field type '+field.type
);
}
export default Field;

View File

@@ -0,0 +1,311 @@
import {create} from 'zustand';
import {produce} from 'immer';
import * as rsssl_api from "../utils/api";
import {__} from '@wordpress/i18n';
import {toast} from 'react-toastify';
import {ReactConditions} from "../utils/ReactConditions";
const fetchFields = () => {
return rsssl_api.getFields().then((response) => {
let fields = response.fields;
let progress = response.progress;
let error = response.error;
return {fields, progress, error};
}).catch((error) => {
console.error(error);
});
}
const useFields = create(( set, get ) => ({
fieldsLoaded: false,
error:false,
fields: [],
changedFields:[],
progress:[],
nextButtonDisabled:false,
overrideNextButtonDisabled:false,
refreshTests:false,
highLightField: '',
setHighLightField: (highLightField) => {
set({ highLightField });
},
setRefreshTests: (refreshTests) => set(state => ({ refreshTests })),
handleNextButtonDisabled: (nextButtonDisabled) => {
set({overrideNextButtonDisabled: nextButtonDisabled});
},
setChangedField: (id, value) => {
set(
produce((state) => {
//remove current reference
const existingFieldIndex = state.changedFields.findIndex(field => {
return field.id===id;
});
if (existingFieldIndex!==-1){
state.changedFields.splice(existingFieldIndex, 1);
}
//add again, with new value
let field = {};
field.id = id;
field.value = value;
state.changedFields.push(field);
})
)
},
showSavedSettingsNotice : (text , type = 'success') => {
handleShowSavedSettingsNotice(text, type);
},
updateField: (id, value) => {
set(
produce((state) => {
let index = state.fields.findIndex(fieldItem => fieldItem.id === id);
if (index !== -1) {
state.fields[index].value = value;
}
})
)
},
updateFieldAttribute: (id, attribute, value) => {
set(
produce((state) => {
let index = state.fields.findIndex(fieldItem => fieldItem.id === id);
if (index !== -1) {
state.fields[index][attribute] = value;
}
})
)
},
updateSubField: (id, subItemId, value) => {
set(
produce((state) => {
let index = state.fields.findIndex(fieldItem => fieldItem.id === id);
let itemValue = state.fields[index].value;
if (!Array.isArray(itemValue)) {
itemValue = [];
}
let subIndex = itemValue.findIndex(subItem => subItem.id === subItemId);
if (subIndex !== -1) {
state.fields[index].updateItemId = subItemId;
state.fields[index].value[subIndex]['value'] = value;
state.fields[index].value = itemValue.map(item => {
const { deleteControl, valueControl, statusControl, ...rest } = item;
return rest;
});
}
})
)
},
removeHelpNotice: (id) => {
set(
produce((state) => {
const fieldIndex = state.fields.findIndex(field => {
return field.id===id;
});
state.fields[fieldIndex].help = false;
})
)
},
addHelpNotice : (id, label, text, title, url) => {
get().removeHelpNotice(id);
//create help object
let help = {};
help.label=label;
help.text=text;
if (url) help.url=url;
if (title) help.title=title;
set(
produce((state) => {
const fieldIndex = state.fields.findIndex(field => {
return field.id===id;
});
if (fieldIndex!==-1) {
state.fields[fieldIndex].help = help;
}
})
)
},
fieldAlreadyEnabled: (id) => {
let fieldIsChanged = get().changedFields.filter(field => field.id === id ).length>0;
let fieldIsEnabled = get().getFieldValue(id);
return !fieldIsChanged && fieldIsEnabled;
},
getFieldValue : (id) => {
let fields = get().fields;
let fieldItem = fields.filter(field => field.id === id )[0];
if (fieldItem){
return fieldItem.value;
}
return false;
},
getField : (id) => {
let fields = get().fields;
let fieldItem = fields.filter(field => field.id === id )[0];
if (fieldItem){
return fieldItem;
}
return false;
},
saveFields: async (skipRefreshTests, showSavedNotice, force = false) => {
let refreshTests = typeof skipRefreshTests !== 'undefined' ? skipRefreshTests : true;
showSavedNotice = typeof showSavedNotice !== 'undefined' ? showSavedNotice : true;
let fields = get().fields;
fields = fields.filter(field => field.data_target !== 'banner');
let changedFields = get().changedFields;
let saveFields = [];
//data_target
for (const field of fields) {
let fieldIsIncluded = changedFields.filter(changedField => changedField.id === field.id).length > 0;
//also check if there's no saved value yet for radio fields, by checking the never_saved attribute.
//a radio or select field looks like it's completed, but won't save if it isn't changed.
//this should not be the case for disabled fields, as these fields often are enabled server side because they're enabled outside Really Simple Security.
let select_or_radio = field.type === 'select' || field.type === 'radio';
if (fieldIsIncluded || (field.never_saved && !field.disabled && select_or_radio)) {
saveFields.push(field);
}
}
//if no fields were changed, do nothing.
if (saveFields.length > 0 || force === true) {
let response = rsssl_api.setFields(saveFields).then((response) => {
return response;
})
if (showSavedNotice) {
toast.promise(
response,
{
pending: __('Saving settings...', 'really-simple-ssl'),
success: __('Settings saved', 'really-simple-ssl'),
error: __('Something went wrong', 'really-simple-ssl'),
}
);
}
await response.then((response) => {
set(
produce((state) => {
state.changedFields = [];
state.fields = response.fields;
state.progress = response.progress;
state.refreshTests = refreshTests;
})
)
});
}
if (showSavedNotice && saveFields.length === 0) {
//nothing to save. show instant success.
toast.promise(
Promise.resolve(),
{
success: __('Settings saved', 'really-simple-ssl'),
}
);
}
},
updateFieldsData: (selectedSubMenuItem) => {
let fields = get().fields;
fields = updateFieldsListWithConditions(fields);
//only if selectedSubMenuItem is actually passed
if (selectedSubMenuItem) {
let nextButtonDisabled = isNextButtonDisabled(fields, selectedSubMenuItem);
//if the button was set to disabled with the handleNextButtonDisabled function, we give that priority until it's released.
if (get().overrideNextButtonDisabled) {
nextButtonDisabled = get().overrideNextButtonDisabled;
}
set(
produce((state) => {
state.nextButtonDisabled = nextButtonDisabled;
})
)
}
set(
produce((state) => {
state.fields = fields;
})
)
},
fetchFieldsData: async ( selectedSubMenuItem ) => {
const { fields, progress, error } = await fetchFields();
let conditionallyEnabledFields = updateFieldsListWithConditions(fields);
let selectedFields = conditionallyEnabledFields.filter(field => field.menu_id === selectedSubMenuItem);
set({fieldsLoaded: true, fields:conditionallyEnabledFields, selectedFields:selectedFields, progress:progress, error: error });
}
}));
export default useFields;
//check if all required fields have been enabled. If so, enable save/continue button
const isNextButtonDisabled = (fields, selectedMenuItem) => {
let fieldsOnPage = [];
//get all fields with group_id this.props.group_id
for (const field of fields){
if (field.menu_id === selectedMenuItem ){
fieldsOnPage.push(field);
}
}
let requiredFields = fieldsOnPage.filter(field => field.required && !field.conditionallyDisabled && (field.value.length==0 || !field.value) );
return requiredFields.length > 0;
}
const updateFieldsListWithConditions = (fields) => {
let newFields = [];
if (!fields || !Array.isArray(fields)) {
return [];
}
fields.forEach(function(field, i) {
// Create instance to evaluate the react_conditions of a field
let reactConditions = new ReactConditions(field, fields);
let fieldCurrentlyEnabled = reactConditions.isEnabled();
//we want to update the changed fields if this field has just become visible. Otherwise the new field won't get saved.
const newField = {...field};
newField.conditionallyDisabled = (fieldCurrentlyEnabled === false);
// ReactConditions will keep the original disabledTooltipText if the
// field is not disabled by the react_conditions. Key differs from the
// one in the config on purpose to prevent overwriting the config value.
newField.disabledTooltipHoverText = reactConditions.getDisabledTooltipText();
// If the field is a letsencrypt field or has a condition_action of
// 'hide', it should be hidden if the field is disabled.
newField.visible = !(!fieldCurrentlyEnabled && (newField.type === 'letsencrypt' || newField.condition_action === 'hide'));
newFields.push(newField);
});
return newFields;
}
const handleShowSavedSettingsNotice = ( text, type ) => {
if (typeof text === 'undefined') {
text = __( 'Settings saved', 'really-simple-ssl' );
}
if (typeof type === 'undefined') {
type = 'success';
}
if (type === 'error') {
toast.error(text);
}
if (type === 'warning') {
toast.warning(text);
}
if (type === 'info') {
toast.info(text);
}
if (type === 'success') {
toast.success(text);
}
}

View File

@@ -0,0 +1,54 @@
import DataTableWrapper from "../DataTable/DataTableWrapper";
import {__} from "@wordpress/i18n";
import DataTableStore from "../DataTable/DataTableStore";
import * as rsssl_api from "../../utils/api";
import useFields from "../FieldsData";
import useMenu from "../../Menu/MenuData";
import {toast} from "react-toastify";
const FileChangeDetection = ({field}) => {
const {
clearAllData,
setProcessing,
} = DataTableStore();
const { updateFieldsData, showSavedSettingsNotice } = useFields();
const { selectedSubMenuItem} = useMenu();
const enabled = true;
const handleClick = async () => {
setProcessing(true);
try {
const response = await rsssl_api.doAction(
'reset_changed_files',
{}
);
} catch (e) {
console.log(e);
} finally {
showSavedSettingsNotice(__('File changes have been been reset', 'really-simple-ssl') );
clearAllData();
setProcessing(false);
//field now should be disabled, as it's now processing
updateFieldsData(selectedSubMenuItem);
}
}
let controlButton = {
show:true,
onClick:handleClick,
label:__("Reset changed files", "really-simple-ssl")
};
return (
<>
<DataTableWrapper
field={field}
controlButton={controlButton}
enabled={true}
/>
</>
)
}
export default FileChangeDetection;

View File

@@ -0,0 +1,17 @@
// FilterData.js
import {create} from 'zustand';
const filterData = create((set, get) => ({
selectedFilter: [],
processingFilter: false,
setSelectedFilter: (selectedFilter, activeGroupId) => {
set((state) => ({
//we make it an array, so we can have multiple filters
selectedFilter: {...state.selectedFilter, [activeGroupId]: selectedFilter},
}));
},
getCurrentFilter: (activeGroupId) => get().selectedFilter[activeGroupId],
setProcessingFilter: (processingFilter) => set({processingFilter}),
}));
export default filterData;

View File

@@ -0,0 +1,19 @@
import Icon from "../../utils/Icon";
const AddButton = ({ getCurrentFilter, moduleName, handleOpen, processing, blockedText, allowedText, disabled }) => {
return (
<div className="rsssl-add-button">
<div className="rsssl-add-button__inner">
<button
className="button button-secondary button-datatable rsssl-add-button__button"
onClick={handleOpen}
disabled={disabled}
>
{allowedText}{processing && <Icon name = "loading" color = 'grey' />}
</button>
</div>
</div>
);
};
export default AddButton;

View File

@@ -0,0 +1,370 @@
import React, { useEffect, useState, useCallback } from '@wordpress/element';
import DataTable, {createTheme} from "react-data-table-component";
import FieldsData from "../FieldsData";
import WhiteListTableStore from "./WhiteListTableStore";
import FilterData from "../FilterData";
import Flag from "../../utils/Flag/Flag";
import {__} from '@wordpress/i18n';
import useFields from "../FieldsData";
import AddButton from "./AddButton";
import TrustIpAddressModal from "./TrustIpAddressModal";
const BlockListDatatable = (props) => {
const {
BlockListData,
WhiteListTable,
setDataLoaded,
dataLoaded,
dataLoaded_block,
fetchData,
processing_block,
ipAddress,
pagination,
resetRow,
rowCleared,
} = WhiteListTableStore();
const {showSavedSettingsNotice, saveFields} = FieldsData();
const [rowsSelected, setRowsSelected] = useState([]);
const [modalOpen, setModalOpen] = useState(false);
const [tableHeight, setTableHeight] = useState(600); // Starting height
const rowHeight = 50; // Height of each row.
const moduleName = 'rsssl-group-filter-firewall_block_list_listing';
const {fields, fieldAlreadyEnabled, getFieldValue} = useFields();
const [searchTerm, setSearchTerm] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [DataTable, setDataTable] = useState(null);
const [theme, setTheme] = useState(null);
useEffect(() => {
import('react-data-table-component').then((module) => {
const { default: DataTable, createTheme } = module;
setDataTable(() => DataTable);
setTheme(() => createTheme('really-simple-plugins', {
divider: {
default: 'transparent',
},
}, 'light'));
});
}, []);
const handlePageChange = (page) => {
setCurrentPage(page);
};
const handlePerRowsChange = (newRowsPerPage) => {
setRowsPerPage(newRowsPerPage);
};
const {
getCurrentFilter,
} = FilterData();
const [filter, setFilter] = useState(getCurrentFilter(moduleName));
/**
* Build a column configuration object.
*
* @param {object} column - The column object.
* @param {string} column.name - The name of the column.
* @param {boolean} column.sortable - Whether the column is sortable.
* @param {boolean} column.searchable - Whether the column is searchable.
* @param {number} column.width - The width of the column.
* @param {boolean} column.visible - Whether the column is visible.
* @param {string} column.column - The column identifier.
*
* @returns {object} The column configuration object.
*/
const buildColumn = useCallback((column) => ({
//if the filter is set to region and the columns = status we do not want to show the column
name: column.name,
sortable: column.sortable,
searchable: column.searchable,
width: column.width,
visible: column.visible,
column: column.column,
selector: row => row[column.column],
}), [filter]);
let field = props.field;
const columns = field.columns.map(buildColumn);
const searchableColumns = columns
.filter(column => column.searchable)
.map(column => column.column);
useEffect(() => {
setRowsSelected([]);
}, [BlockListData]);
let enabled = getFieldValue('enable_firewall');
useEffect(() => {
const currentFilter = getCurrentFilter(moduleName);
if (typeof currentFilter === 'undefined') {
setFilter('all');
} else {
setFilter(currentFilter);
}
setRowsSelected([]);
// resetRowSelection(true);
// resetRowSelection(false);
}, [getCurrentFilter(moduleName)]);
useEffect(() => {
return () => {
saveFields(false, false)
};
}, [enabled]);
useEffect(() => {
if (typeof filter !== 'undefined') {
fetchData(field.action, filter);
}
}, [filter]);
useEffect(() => {
if(!dataLoaded_block) {
fetchData(field.action, filter);
}
}, [dataLoaded_block]);
const customStyles = {
headCells: {
style: {
paddingLeft: '0',
paddingRight: '0',
},
},
cells: {
style: {
paddingLeft: '0',
paddingRight: '0',
},
},
};
createTheme('really-simple-plugins', {
divider: {
default: 'transparent',
},
}, 'light');
const handleSelection = useCallback((state) => {
//based on the current page and the rows per page we get the rows that are selected
const {selectedCount, selectedRows, allSelected, allRowsSelected} = state;
let rows = [];
if (allSelected) {
rows = selectedRows.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage);
setRowsSelected(rows);
} else {
setRowsSelected(selectedRows);
}
}, [currentPage, rowsPerPage]);
const allowById = useCallback((id) => {
//We check if the id is an array
if (Array.isArray(id)) {
//We get all the iso2 codes and names from the array
const ids = id.map(item => ({
id: item.id,
}));
//we loop through the ids and allow them one by one
ids.forEach((id) => {
resetRow(id.id).then((result) => {
showSavedSettingsNotice(result.message);
});
});
// fetchData(field.action, filter ? filter : 'all');
setRowsSelected([]);
} else {
resetRow(id).then((result) => {
showSavedSettingsNotice(result.message);
});
// fetchData(field.action, filter ? filter : 'all');
}
setDataLoaded(false);
}, [resetRow]);
const data = {...BlockListData.data};
const generateFlag = useCallback((flag, title) => (
<>
<Flag
countryCode={flag}
style={{
fontSize: '2em',
}}
title={title}
/>
</>
), []);
const ActionButton = ({ onClick, children, className }) => (
// <div className={`rsssl-action-buttons__inner`}>
<button
className={`button ${className} rsssl-action-buttons__button`}
onClick={onClick}
disabled={processing_block}
>
{children}
</button>
// </div>
);
const handleClose = () => {
setModalOpen(false);
}
const handleOpen = () => {
setModalOpen(true);
}
const generateActionButtons = useCallback((id) => {
return (<div className="rsssl-action-buttons">
<ActionButton
onClick={() => allowById(id)} className="button-red">
{__("Reset", "really-simple-ssl")}
</ActionButton>
</div>)
}, [moduleName, allowById]);
for (const key in data) {
const dataItem = {...data[key]};
dataItem.action = generateActionButtons(dataItem.id);
dataItem.flag = generateFlag(dataItem.iso2_code, dataItem.country_name);
dataItem.status = __(dataItem.status = dataItem.status.charAt(0).toUpperCase() + dataItem.status.slice(1), 'really-simple-ssl');
data[key] = dataItem;
}
let paginationSet;
paginationSet = typeof pagination !== 'undefined';
useEffect(() => {
if (Object.keys(data).length === 0 ) {
setTableHeight(100); // Adjust depending on your UI measurements
} else {
setTableHeight(rowHeight * (paginationSet ? pagination.perPage + 2 : 12)); // Adjust depending on your UI measurements
}
}, [paginationSet, pagination?.perPage, data]);
useEffect(() => {
let intervals = [];
const filteredData = Object.entries(data)
.filter(([_, dataItem]) => {
return Object.values(dataItem).some(val => ((val ?? '').toString().toLowerCase().includes(searchTerm.toLowerCase())));
})
.map(([key, dataItem]) => {
const newItem = { ...dataItem };
newItem.action = generateActionButtons(newItem.id);
newItem.flag = generateFlag(newItem.iso2_code, newItem.country_name);
newItem.status = __(newItem.status = newItem.status.charAt(0).toUpperCase() + newItem.status.slice(1), 'really-simple-ssl');
// if the newItem.time_left not is 0 we count down in seconds the value
if (newItem.time_left > 0) {
const interval = setInterval(() => {
newItem.time_left--;
}, 1000);
intervals.push(interval);
}
return [key, newItem];
})
.reduce((obj, [key, val]) => ({ ...obj, [key]: val }), {});
}, [searchTerm, data]);
return (
<>
<TrustIpAddressModal
isOpen={modalOpen}
onRequestClose={handleClose}
value={ipAddress}
status={'blocked'}
filter={filter? filter : 'all'}
>
</TrustIpAddressModal>
<div className="rsssl-container">
{/*display the add button on left side*/}
<AddButton
moduleName={moduleName}
handleOpen={handleOpen}
processing={processing_block}
allowedText={__("Block IP Address", "really-simple-ssl")}
/>
<div className="rsssl-search-bar">
<div className="rsssl-search-bar__inner">
<div className="rsssl-search-bar__icon"></div>
<input
type="text"
className="rsssl-search-bar__input"
placeholder={__("Search", "really-simple-ssl")}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
</div>
</div>
{rowsSelected.length > 0 && (
<div
style={{
marginTop: '1em',
marginBottom: '1em',
}}
>
<div className={"rsssl-multiselect-datatable-form rsssl-primary"}>
<div>
{__("You have selected %s rows", "really-simple-ssl").replace('%s', rowsSelected.length)}
</div>
<div className="rsssl-action-buttons">
<>
<ActionButton
onClick={() => allowById(rowsSelected)} className="button-red">
{__("Reset", "really-simple-ssl")}
</ActionButton>
</>
</div>
</div>
</div>
)}
{DataTable &&
<DataTable
columns={columns}
data={Object.values(data).filter((row) => {
return Object.values(row).some((val) => ((val ?? '').toString().toLowerCase()).includes(searchTerm.toLowerCase()));
})}
dense
pagination={true}
paginationComponentOptions={{
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
rangeSeparatorText: __('of', 'really-simple-ssl'),
noRowsPerPage: false,
selectAllRowsItem: false,
selectAllRowsItemText: __('All', 'really-simple-ssl'),
}}
noDataComponent={__("No results", "really-simple-ssl")}
persistTableHead
selectableRows={true}
clearSelectedRows={rowCleared}
paginationPerPage={rowsPerPage}
onChangePage={handlePageChange}
onChangeRowsPerPage={handlePerRowsChange}
onSelectedRowsChange={handleSelection}
theme="really-simple-plugins"
customStyles={customStyles}
/>}
{!enabled && (
<div className="rsssl-locked">
<div className="rsssl-locked-overlay"><span
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Here you can add IP addresses that should never be blocked by region restrictions.', 'really-simple-ssl')}</span>
</div>
</div>
)}
</>
);
}
export default BlockListDatatable;

View File

@@ -0,0 +1,316 @@
/* Creates A Store For Risk Data using Zustand */
import {create} from 'zustand';
import * as rsssl_api from "../../utils/api";
import {__} from "@wordpress/i18n";
import {produce} from "immer";
import React from "react";
import GeoDatatable from "./GeoDatatable";
const GeoDataTableStore = create((set, get) => ({
processing: false,
dataLoaded: false,
pagination: {},
dataActions: {},
CountryDataTable: [],
rowCleared: false,
fetchCountryData: async (action, filterValue) => {
//we check if the processing is already true, if so we return
set({
processing: true,
rowCleared: true,
dataLoaded: false
});
// if the filterValue is not set, we do nothing.
if (!filterValue) {
set({processing: false});
return;
}
try {
const response = await rsssl_api.doAction(
action, {filterValue}
);
//now we set the EventLog
if (response && response.request_success) {
set({
CountryDataTable: response,
dataLoaded: true,
processing: false
});
}
set({ rowCleared: true });
} catch (e) {
console.error(e);
}
},
handleCountryTableSearch: async (search, searchColumns) => {
//Add the search to the dataActions
set(produce((state) => {
state.dataActions = {...state.dataActions, search, searchColumns};
})
);
},
handleCountryTablePageChange: async (page, pageSize) => {
//Add the page and pageSize to the dataActions
set(produce((state) => {
state.dataActions = {...state.dataActions, page, pageSize};
})
);
},
handleCountryTableRowsChange: async (currentRowsPerPage, currentPage) => {
//Add the page and pageSize to the dataActions
set(produce((state) => {
state.dataActions = {...state.dataActions, currentRowsPerPage, currentPage};
})
);
},
//this handles all pagination and sorting
handleCountryTableSort: async (column, sortDirection) => {
//Add the column and sortDirection to the dataActions
set(produce((state) => {
state.dataActions = {...state.dataActions, sortColumn: column, sortDirection};
})
);
},
handleCountryTableFilter: async (column, filterValue) => {
//Add the column and sortDirection to the dataActions
set(produce((state) => {
state.dataActions = {...state.dataActions, filterColumn: column, filterValue};
})
);
},
/*
* This function add a new row to the table
*/
addRow: async (country, name) => {
set({rowCleared: false});
let data = {
country_code: country,
country_name: name
};
try {
const response = await rsssl_api.doAction('geo_block_add_blocked_country', data);
if (response && response.request_success) {
set({rowCleared: true});
return { success: true, message: response.message, response };
} else {
// Return a custom error message or the API response message.
return { success: false, message: response?.message || 'Failed to add country', response };
}
} catch (e) {
console.error(e);
// Return the caught error with a custom message.
return { success: false, message: 'Error occurred', error: e };
}
},
addMultiRow: async (countries) => {
set({processing: true});
set({rowCleared: false});
let data = {
country_codes: countries
};
try {
const response = await rsssl_api.doAction('geo_block_add_blocked_country', data);
if (response && response.request_success) {
set({rowCleared: true});
// Return the success message from the API response.
return { success: true, message: response.message, response };
} else {
set({rowCleared: true});
// Return a custom error message or the API response message.
return { success: false, message: response?.message || 'Failed to add countries', response };
}
} catch (e) {
console.error(e);
set({rowCleared: true});
// Return the caught error with a custom message.
return { success: false, message: 'Error occurred', error: e };
} finally {
set({processing: false});
set({rowCleared: true});
}
},
removeRowMulti: async (countries, dataActions) => {
set({processing: true});
set({rowCleared: false});
let data = {
country_codes: countries
};
try {
const response = await rsssl_api.doAction('geo_block_remove_blocked_country', data);
if (response && response.request_success) {
set({rowCleared: true});
// Return the success message from the API response.
return { success: true, message: response.message, response };
} else {
// Return a custom error message or the API response message.
return { success: false, message: response?.message || 'Failed to remove countries', response };
}
}
catch (e) {
console.error(e);
set({rowCleared: true});
// Return the caught error with a custom message.
return { success: false, message: 'Error occurred', error: e };
} finally {
set({rowCleared: true});
set({processing: false});
}
},
removeRow: async (country) => {
set({processing: true});
set({rowCleared: false});
let data = {
country_code: country
};
try {
const response = await rsssl_api.doAction('geo_block_remove_blocked_country', data);
// Consider checking the response structure for any specific success or failure signals
if (response && response.request_success) {
set({rowCleared: true});
// Potentially notify the user of success, if needed.
return { success: true, message: response.message, response };
} else {
// Handle any unsuccessful response if needed.
set({rowCleared: true});
return { success: false, message: response?.message || 'Failed to remove country', response };
}
} catch (e) {
console.error(e);
// Notify the user of an error.
return { success: false, message: 'Error occurred', error: e };
} finally {
set({rowCleared: true});
set({processing: false});
}
},
addRegion: async (region) => {
set({processing: true});
set({rowCleared: false});
let data = {
region_code: region
};
try {
const response = await rsssl_api.doAction('geo_block_add_blocked_region', data);
// Consider checking the response structure for any specific success or failure signals
if (response && response.request_success) {
set({rowCleared: true});
// Potentially notify the user of success, if needed.
return { success: true, message: response.message, response };
} else {
// Handle any unsuccessful response if needed.
set({rowCleared: true});
return { success: false, message: response?.message || 'Failed to add region', response };
}
} catch (e) {
console.error(e);
// Notify the user of an error.
return { success: false, message: 'Error occurred', error: e };
} finally {
set({processing: false});
set({rowCleared: true});
}
},
addRegionsMulti: async (regions, dataActions) => {
set({processing: true});
set({rowCleared: false});
let data = {
region_codes: regions
};
try {
const response = await rsssl_api.doAction('geo_block_add_blocked_region', data);
// Consider checking the response structure for any specific success or failure signals
if (response && response.request_success) {
set({rowCleared: true});
// Potentially notify the user of success, if needed.
return { success: true, message: response.message, response };
} else {
set({rowCleared: true});
// Handle any unsuccessful response if needed.
return { success: false, message: response?.message || 'Failed to add regions', response };
}
} catch (e) {
console.error(e);
// Notify the user of an error.
return { success: false, message: 'Error occurred', error: e };
} finally {
set({rowCleared: true});
set({processing: false});
}
},
removeRegion: async (region) => {
set({processing: true});
set({rowCleared: false});
let data = {
region_code: region
};
try {
const response = await rsssl_api.doAction('geo_block_remove_blocked_region', data);
// Consider checking the response structure for any specific success or failure signals
if (response && response.request_success) {
set({rowCleared: true});
// Potentially notify the user of success, if needed.
return { success: true, message: response.message, response };
} else {
// Handle any unsuccessful response if needed.
set({rowCleared: true});
return { success: false, message: response?.message || 'Failed to remove region', response };
}
} catch (e) {
console.error(e);
// Notify the user of an error.
return { success: false, message: 'Error occurred', error: e };
} finally {
set({processing: false});
set({rowCleared: true});
}
},
removeRegionMulti: async (regions) => {
set({processing: true});
set({rowCleared: false});
let data = {
region_codes: regions
};
try {
const response = await rsssl_api.doAction('geo_block_remove_blocked_region', data);
// Consider checking the response structure for any specific success or failure signals
if (response && response.request_success) {
// Potentially notify the user of success, if needed.
set({rowCleared: true});
return { success: true, message: response.message, response };
} else {
set({rowCleared: true});
// Handle any unsuccessful response if needed.
return { success: false, message: response?.message || 'Failed to remove regions', response };
}
} catch (e) {
console.error(e);
// Notify the user of an error.
set({rowCleared: true});
return { success: false, message: 'Error occurred', error: e };
} finally {
set({processing: false});
set({rowCleared: true});
}
},
resetRowSelection: async (on_off) => {
set({rowCleared: on_off});
}
}));
export default GeoDataTableStore;

View File

@@ -0,0 +1,528 @@
import {useEffect, useState, useCallback} from '@wordpress/element';
import FieldsData from "../FieldsData";
import GeoDataTableStore from "./GeoDataTableStore";
import EventLogDataTableStore from "../EventLog/EventLogDataTableStore";
import FilterData from "../FilterData";
import Flag from "../../utils/Flag/Flag";
import {__} from '@wordpress/i18n';
import useFields from "../FieldsData";
import useMenu from "../../Menu/MenuData";
/**
* A component for displaying a geo datatable.
*
* @param {Object} props - The component props.
* @param {string} props.field - The field to display.
*
* @returns {JSX.Element} The rendered component.
*/
const GeoDatatable = (props) => {
const {
CountryDataTable,
dataLoaded,
fetchCountryData,
addRow,
addMultiRow,
removeRegion,
removeRegionMulti,
addRegion,
addRegionsMulti,
removeRow,
removeRowMulti,
rowCleared,
resetRowSelection,
} = GeoDataTableStore();
const moduleName = 'rsssl-group-filter-firewall_list_listing';
const [localData, setLocalData] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [visualData, setVisualData] = useState([]);
const {showSavedSettingsNotice, saveFields} = FieldsData();
const [rowsSelected, setRowsSelected] = useState([]);
const [columns, setColumns] = useState([]);
const {fields, fieldAlreadyEnabled, getFieldValue, setHighLightField, getField} = useFields();
const [currentPage, setCurrentPage] = useState(1);
const [rowsPerPage, setRowsPerPage] = useState(10);
const {setSelectedSubMenuItem} = useMenu();
const [DataTable, setDataTable] = useState(null);
const [theme, setTheme] = useState(null);
useEffect( () => {
import('react-data-table-component').then(({ default: DataTable, createTheme }) => {
setDataTable(() => DataTable);
setTheme(() => createTheme('really-simple-plugins', {
divider: {
default: 'transparent',
},
}, 'light'));
});
}, []);
let enabled = getFieldValue('enable_firewall');
const handlePageChange = (page) => {
setCurrentPage(page);
};
const handlePerRowsChange = (newRowsPerPage) => {
setRowsPerPage(newRowsPerPage);
};
const {
selectedFilter,
setSelectedFilter,
activeGroupId,
getCurrentFilter,
setProcessingFilter,
} = FilterData();
const [filter, setFilter] = useState(getCurrentFilter(moduleName));
const buildColumn = useCallback((column) => ({
//if the filter is set to region and the columns = status we do not want to show the column
omit: filter === 'regions' && (column.column === 'country_name' || column.column === 'flag'),
name: (column.column === 'action' && 'regions' === filter) ? __('Block / Allow All', 'really-simple-ssl') : column.name,
sortable: column.sortable,
searchable: column.searchable,
width: column.width,
visible: column.visible,
column: column.column,
selector: row => row[column.column],
}), [filter]);
let field = props.field;
useEffect(() => {
const element = document.getElementById('set_to_captcha_configuration');
const clickListener = async event => {
event.preventDefault();
if (element) {
await redirectToAddCaptcha(element);
}
};
if (element) {
element.addEventListener('click', clickListener);
}
return () => {
if (element) {
element.removeEventListener('click', clickListener);
}
};
}, []);
const redirectToAddCaptcha = async (element) => {
// We fetch the props from the menu item
let menuItem = getField('enabled_captcha_provider');
// Create a new object based on the menuItem, including the new property
let highlightingMenuItem = {
...menuItem,
highlight_field_id: 'enabled_captcha_provider',
};
setHighLightField(highlightingMenuItem.highlight_field_id);
let highlightField = getField(highlightingMenuItem.highlight_field_id);
await setSelectedSubMenuItem(highlightField.menu_id);
}
const blockCountryByCode = useCallback(async (code, name) => {
if (Array.isArray(code)) {
//We get all the iso2 codes and names from the array
const ids = code.map(item => ({
country_code: item.iso2_code,
country_name: item.country_name
}));
//we loop through the ids and block them one by one
await addMultiRow(ids).then(
(response) => {
if (response.success) {
showSavedSettingsNotice(response.message);
} else {
showSavedSettingsNotice(response.message, 'error');
}
}
);
await fetchCountryData(field.action, filter);
setRowsSelected([]);
} else {
await addRow(code, name).then((result) => {
showSavedSettingsNotice(result.message);
if (result.success) {
fetchCountryData(field.action, filter);
}
});
}
}, [addRow, filter, localData, enabled]);
const allowRegionByCode = useCallback(async (code, regionName = '') => {
if (Array.isArray(code)) {
const ids = code.map(item => ({
iso2_code: item.iso2_code,
country_name: item.country_name
}));
await removeRegionMulti(ids).then(
(response) => {
if (response.success) {
showSavedSettingsNotice(response.message);
if (response.success) {
fetchCountryData(field.action, filter);
}
} else {
showSavedSettingsNotice(response.message, 'error');
}
}
);
setRowsSelected([]);
} else {
await removeRegion(code).then((result) => {
showSavedSettingsNotice(result.message);
if (result.success) {
fetchCountryData(field.action, filter);
}
});
}
}, [removeRegion, filter]);
const blockRegionByCode = useCallback(async (code, region = '') => {
if (Array.isArray(code)) {
const ids = code.map(item => ({
iso2_code: item.iso2_code,
country_name: item.country_name
}));
await addRegionsMulti(ids).then(
(response) => {
if (response.success) {
showSavedSettingsNotice(response.message);
} else {
showSavedSettingsNotice(response.message, 'error');
}
}
);
await fetchCountryData(field.action, filter);
setRowsSelected([]);
} else {
await addRegion(code).then((result) => {
if (result.success) {
showSavedSettingsNotice(result.message);
} else {
showSavedSettingsNotice(result.message, 'error');
}
});
await fetchCountryData(field.action, filter);
}
}, [addRegion, filter]);
const allowCountryByCode = useCallback(async (code) => {
if (Array.isArray(code)) {
const ids = code.map(item => ({
country_code: item.iso2_code,
country_name: item.country_name
}));
//we loop through the ids and allow them one by one
await removeRowMulti(ids).then(
(response) => {
if (response.success) {
showSavedSettingsNotice(response.message);
} else {
showSavedSettingsNotice(response.message, 'error');
}
}
);
setRowsSelected([]);
await fetchCountryData(field.action, filter);
} else {
await removeRow(code).then((result) => {
showSavedSettingsNotice(result.message);
});
await fetchCountryData(field.action, filter);
}
}, [removeRow, filter]);
const ActionButton = ({onClick, children, className, disabled = false}) => (
// <div className={`rsssl-action-buttons__inner`}>
<button
className={`button ${className} rsssl-action-buttons__button`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
// </div>
);
const customStyles = {
headCells: {
style: {
paddingLeft: '0',
paddingRight: '0',
},
},
cells: {
style: {
paddingLeft: '0',
paddingRight: '0',
},
},
};
const generateActionButtons = useCallback((code, name, region_name, showBlockButton = true, showAllowButton = true) => {
return (<div className="rsssl-action-buttons">
{filter === 'blocked' && (
<ActionButton
onClick={() => allowCountryByCode(code)}
className="button-secondary">
{__("Allow", "really-simple-ssl")}
</ActionButton>
)}
{filter === 'regions' && (
<>
<ActionButton
onClick={() => blockRegionByCode(code, region_name)}
className="button-primary"
disabled={!showBlockButton}
>
{__("Block", "really-simple-ssl")}
</ActionButton>
<ActionButton
onClick={() => allowRegionByCode(code, region_name)}
className="button-secondary"
disabled={!showAllowButton}
>
{__("Allow", "really-simple-ssl")}
</ActionButton>
</>
)}
{filter === 'countries' && (
<ActionButton
onClick={() => blockCountryByCode(code, name)}
className="button-primary">
{__("Block", "really-simple-ssl")}
</ActionButton>
)}
</div>)
}, [filter]);
const generateFlag = useCallback((flag, title) => {
return (
<>
<Flag
countryCode={flag}
style={{
fontSize: '2em',
}}
title={title}
continent={(getCurrentFilter(moduleName) === 'regions')}
/>
</>
)
}, [filter]);
useEffect(() => {
const currentFilter = getCurrentFilter(moduleName);
if (typeof currentFilter === 'undefined') {
setFilter('regions');
setSelectedFilter('regions', moduleName);
} else {
setFilter(currentFilter);
}
setRowsSelected([]);
resetRowSelection(true);
resetRowSelection(false);
}, [getCurrentFilter(moduleName)]);
useEffect(() => {
if (filter !== undefined) {
const fetchData = async () => {
await fetchCountryData(field.action, filter);
}
fetchData();
}
}, [filter]);
useEffect(() => {
if (dataLoaded && CountryDataTable.data !== undefined) {
setLocalData(CountryDataTable.data);
}
}, [dataLoaded]);
const handleSelection = useCallback((state) => {
//based on the current page and the rows per page we get the rows that are selected
const {selectedCount, selectedRows, allSelected, allRowsSelected} = state;
let rows = [];
if (allSelected) {
rows = selectedRows.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage);
setRowsSelected(rows);
} else {
setRowsSelected(selectedRows);
}
}, [currentPage, rowsPerPage, visualData]);
useEffect(() => {
let FilterColumns = field.columns.map(buildColumn);
// Find the index of the 'action' column
const actionIndex = FilterColumns.findIndex(column => column.column === 'action');
// If 'filter' equals 'regions' and 'action' column exists, then do the rearrangement
if (filter === 'regions' && actionIndex !== -1) {
const actionColumn = FilterColumns[actionIndex];
// Remove 'action' column from its current place
FilterColumns.splice(actionIndex, 1);
const emptyColumn = {
name: '',
selector: '',
sortable: false,
omit: false,
searchable: false,
};
// Push 'action' column to the end of the array
FilterColumns.push(emptyColumn, actionColumn);
}
setColumns(FilterColumns);
const generatedVisualData = (localData || [])
.filter((row) => {
return Object.values(row).some((val) => ((val ?? '').toString().toLowerCase()).includes(searchTerm.toLowerCase()));
}).map((row) => {
const newRow = {...row};
columns.forEach((column) => {
newRow[column.column] = row[column.column];
});
if (filter === 'regions') {
let showBlockButton = (newRow.region_count - newRow.blocked_count) > 0;
let showAllowButton = (newRow.blocked_count > 0);
newRow.action = generateActionButtons(newRow.iso2_code, newRow.country_name, newRow.region, showBlockButton, showAllowButton);
} else if (filter === 'countries') {
newRow.action = generateActionButtons(newRow.iso2_code, newRow.country_name, newRow.region);
} else {
newRow.action = generateActionButtons(newRow.iso2_code, newRow.status, newRow.region);
}
newRow.flag = generateFlag(newRow.iso2_code, newRow.country_name);
if (newRow.status) {
newRow.status = __(newRow.status.charAt(0).toUpperCase() + newRow.status.slice(1), 'really-simple-ssl');
if ('regions' === filter) {
// So i all is blocked we don't want to show the count also if all are allowed we don't want to show the count
if (newRow.blocked_count === newRow.region_count || newRow.blocked_count === 0) {
newRow.status = newRow.status;
} else {
newRow.status = newRow.status + ' (' + newRow.blocked_count + '/ ' + newRow.region_count + ')';
}
}
}
return newRow;
});
setVisualData(generatedVisualData);
}, [localData, searchTerm]);
useEffect(() => {
if ( rowsSelected.length === 0 ) {
resetRowSelection
}
}, [rowsSelected]);
return (
<>
<div className="rsssl-container">
<div>
{/* reserved for left side buttons */}
</div>
<div className="rsssl-search-bar">
<div className="rsssl-search-bar__inner">
<div className="rsssl-search-bar__icon"></div>
<input
type="text"
className="rsssl-search-bar__input"
placeholder={__("Search", "really-simple-ssl")}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
</div>
</div>
{rowsSelected.length > 0 && (
<div
style={{
marginTop: '1em',
marginBottom: '1em',
}}
>
<div className={"rsssl-multiselect-datatable-form rsssl-primary"}>
<div>
{__("You have selected %s rows", "really-simple-ssl").replace('%s', rowsSelected.length)}
</div>
<div className="rsssl-action-buttons">
{filter === 'countries' && (
<>
<ActionButton
onClick={() => blockCountryByCode(rowsSelected)} className="button-primary">
{__("Block", "really-simple-ssl")}
</ActionButton>
</>
)}
{filter === 'regions' && (
<>
<ActionButton
onClick={() => allowRegionByCode(rowsSelected)} className="button-secondary">
{__("Allow", "really-simple-ssl")}
</ActionButton>
<ActionButton
onClick={() => blockRegionByCode(rowsSelected)} className="button-primary">
{__("Block", "really-simple-ssl")}
</ActionButton>
</>
)}
{filter === 'blocked' && (
<ActionButton
onClick={() => allowCountryByCode(rowsSelected)}>
{__("Allow", "really-simple-ssl")}
</ActionButton>
)}
</div>
</div>
</div>
)}
{DataTable &&
<DataTable
columns={columns}
data={visualData}
dense
pagination={true}
paginationComponentOptions={{
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
rangeSeparatorText: __('of', 'really-simple-ssl'),
noRowsPerPage: false,
selectAllRowsItem: false,
selectAllRowsItemText: __('All', 'really-simple-ssl'),
}}
noDataComponent={__("No results", "really-simple-ssl")}
persistTableHead
selectableRows={true}
paginationPerPage={rowsPerPage}
onChangePage={handlePageChange}
onChangeRowsPerPage={handlePerRowsChange}
onSelectedRowsChange={handleSelection}
clearSelectedRows={rowCleared}
theme="really-simple-plugins"
customStyles={customStyles}
>
</DataTable> }
{!getFieldValue('enable_firewall') && (
<div className="rsssl-locked">
<div className="rsssl-locked-overlay"><span
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Restrict access from specific countries or continents. You can also allow only specific countries.', 'really-simple-ssl')}</span>
</div>
</div>
)}
</>
)
}
export default GeoDatatable;

View File

@@ -0,0 +1,156 @@
import {useState} from '@wordpress/element';
import Icon from "../../utils/Icon";
import {
Modal,
Button,
TextControl
} from "@wordpress/components";
import {__} from "@wordpress/i18n";
import FieldsData from "../FieldsData";
import WhiteListTableStore from "./WhiteListTableStore";
const TrustIpAddressModal = (props) => {
const { note, setNote, ipAddress, setIpAddress, maskError, setDataLoaded, dataLoaded, updateRow, resetRange} = WhiteListTableStore();
const [rangeDisplay, setRangeDisplay] = useState(false);
const {showSavedSettingsNotice} = FieldsData();
//we add a function to handle the range fill
const handleRangeFill = () => {
//we toggle the range display.
setRangeDisplay(!rangeDisplay);
}
async function handleSubmit() {
// we check if statusSelected is not empty
if (ipAddress && maskError === false) {
await updateRow(ipAddress, note, props.status ,props.filter).then((response) => {
if (response.success) {
showSavedSettingsNotice(response.message);
//we fetch the data again
setDataLoaded(false);
} else {
showSavedSettingsNotice(response.message, 'error');
}
});
//we clear the input
resetRange();
//we close the modal
props.onRequestClose();
}
}
function handleCancel() {
// Reset all local state
setRangeDisplay(false);
resetRange();
// Close the modal
props.onRequestClose();
}
if (!props.isOpen) {
return null;
}
const changeHandler = (e) => {
if (e.length > 0) {
setIpAddress(e);
} else {
resetRange()
}
}
return (
<Modal
title={__("Add IP Address", "really-simple-ssl")}
shouldCloseOnClickOutside={true}
shouldCloseOnEsc={true}
overlayClassName="rsssl-modal-overlay"
className="rsssl-modal"
onRequestClose={props.onRequestClose}
>
<div className="modal-content">
<div className="modal-body"
style={{
padding: "0.5em",
}}
>
<div
style={{
width: "95%",
height: "100%",
padding: "10px",
}}
>
<div style={{position: 'relative'}}>
<label
htmlFor={'ip-address'}
className={'rsssl-label'}
>{__('IP Address', 'really-simple-ssl')}</label>
<TextControl
id="ip-address"
name="ip-address"
onChange={changeHandler}
value={ipAddress}
/>
<div className="rsssl-ip-verified">
{Boolean(!maskError && ipAddress.length > 0)
? <Icon name='circle-check' color={'green'}/>
: <Icon name='circle-times' color={'red'}/>
}
</div>
</div>
<div>
<label
htmlFor={'note'}
className={'rsssl-label'}
>{__('Notes', 'really-simple-ssl')}</label>
<input
name={'note'}
id={'note'}
type={'text'}
value={note}
onChange={(e) => setNote(e.target.value)}
style={{
width: '100%',
}}
/>
</div>
</div>
</div>
<div className="modal-footer">
{/*//we add two buttons here for add row and cancel*/}
<div
className={'rsssl-grid-item-footer'}
style
={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
padding: '1em',
}
}
>
<Button
isSecondary
onClick={handleCancel}
style={{ marginRight: '10px' }}
>
{__("Cancel", "really-simple-ssl")}
</Button>
<Button
isPrimary
onClick={handleSubmit}
>
{__("Add", "really-simple-ssl")}
</Button>
</div>
</div>
</div>
</Modal>
)
}
export default TrustIpAddressModal;

View File

@@ -0,0 +1,423 @@
import { useEffect, useState, useCallback } from '@wordpress/element';
import DataTable, {createTheme} from "react-data-table-component";
import FieldsData from "../FieldsData";
import WhiteListTableStore from "./WhiteListTableStore";
import Flag from "../../utils/Flag/Flag";
import {__} from '@wordpress/i18n';
import useFields from "../FieldsData";
import AddButton from "./AddButton";
import TrustIpAddressModal from "./TrustIpAddressModal";
const WhiteListDatatable = (props) => {
const {
WhiteListTable,
fetchWhiteListData,
processing,
ipAddress,
addRow,
removeRow,
pagination,
addRegion,
removeRegion,
resetRow,
rowCleared,
} = WhiteListTableStore();
const {showSavedSettingsNotice, saveFields} = FieldsData();
const [rowsSelected, setRowsSelected] = useState([]);
const [modalOpen, setModalOpen] = useState(false);
const [tableHeight, setTableHeight] = useState(600); // Starting height
const rowHeight = 50; // Height of each row.
const moduleName = 'rsssl-group-filter-geo_block_list_white_listing';
const {fields, fieldAlreadyEnabled, getFieldValue} = useFields();
const [searchTerm, setSearchTerm] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [DataTable, setDataTable] = useState(null);
const [theme, setTheme] = useState(null);
useEffect(() => {
import('react-data-table-component').then((module) => {
const { default: DataTable, createTheme } = module;
setDataTable(() => DataTable);
setTheme(() => createTheme('really-simple-plugins', {
divider: {
default: 'transparent',
},
}, 'light'));
});
}, []);
const handlePageChange = (page) => {
setCurrentPage(page);
};
const handlePerRowsChange = (newRowsPerPage) => {
setRowsPerPage(newRowsPerPage);
};
/**
* Build a column configuration object.
*
* @param {object} column - The column object.
* @param {string} column.name - The name of the column.
* @param {boolean} column.sortable - Whether the column is sortable.
* @param {boolean} column.searchable - Whether the column is searchable.
* @param {number} column.width - The width of the column.
* @param {boolean} column.visible - Whether the column is visible.
* @param {string} column.column - The column identifier.
*
* @returns {object} The column configuration object.
*/
const buildColumn = useCallback((column) => ({
//if the filter is set to region and the columns = status we do not want to show the column
name: column.name,
sortable: column.sortable,
searchable: column.searchable,
width: column.width,
visible: column.visible,
column: column.column,
selector: row => row[column.column],
}), []);
let field = props.field;
const columns = field.columns.map(buildColumn);
const searchableColumns = columns
.filter(column => column.searchable)
.map(column => column.column);
useEffect(() => {
setRowsSelected([]);
}, [WhiteListTable]);
useEffect(() => {
fetchWhiteListData(field.action);
}, [fieldAlreadyEnabled('enable_firewall')]);
let enabled = getFieldValue('enable_firewall');
useEffect(() => {
return () => {
saveFields(false, false)
};
}, [enabled]);
const customStyles = {
headCells: {
style: {
paddingLeft: '0',
paddingRight: '0',
},
},
cells: {
style: {
paddingLeft: '0',
paddingRight: '0',
},
},
};
createTheme('really-simple-plugins', {
divider: {
default: 'transparent',
},
}, 'light');
const handleSelection = useCallback((state) => {
//based on the current page and the rows per page we get the rows that are selected
const {selectedCount, selectedRows, allSelected, allRowsSelected} = state;
let rows = [];
if (allSelected) {
rows = selectedRows.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage);
setRowsSelected(rows);
} else {
setRowsSelected(selectedRows);
}
}, [currentPage, rowsPerPage]);
const allowRegionByCode = useCallback(async (code, regionName = '') => {
if (Array.isArray(code)) {
const ids = code.map(item => ({
iso2_code: item.iso2_code,
country_name: item.country_name
}));
ids.forEach((id) => {
removeRegion(id.iso2_code).then((result) => {
showSavedSettingsNotice(result.message);
});
});
setRowsSelected([]);
await fetchWhiteListData(field.action);
setRowsSelected([]);
} else {
await removeRegion(code).then((result) => {
showSavedSettingsNotice(result.message);
});
}
}, [removeRegion]);
const allowById = useCallback((id) => {
//We check if the id is an array
if (Array.isArray(id)) {
//We get all the iso2 codes and names from the array
const ids = id.map(item => ({
id: item.id,
}));
//we loop through the ids and allow them one by one
ids.forEach((id) => {
resetRow(id.id).then((result) => {
showSavedSettingsNotice(result.message);
});
});
fetchWhiteListData(field.action);
setRowsSelected([]);
} else {
resetRow(id).then((result) => {
showSavedSettingsNotice(result.message);
fetchWhiteListData(field.action);
});
}
}, [resetRow]);
const blockRegionByCode = useCallback(async (code, region = '') => {
if (Array.isArray(code)) {
const ids = code.map(item => ({
iso2_code: item.iso2_code,
country_name: item.country_name
}));
ids.forEach((id) => {
addRegion(id.iso2_code).then((result) => {
showSavedSettingsNotice(result.message);
});
});
setRowsSelected([]);
await fetchWhiteListData(field.action);
setRowsSelected([]);
} else {
await addRegion(code).then((result) => {
showSavedSettingsNotice(result.message);
});
}
}, [addRegion]);
const allowCountryByCode = useCallback(async (code) => {
if (Array.isArray(code)) {
const ids = code.map(item => ({
iso2_code: item.iso2_code,
country_name: item.country_name
}));
//we loop through the ids and allow them one by one
ids.forEach((id) => {
removeRow(id.iso2_code).then((result) => {
showSavedSettingsNotice(result.message);
});
});
setRowsSelected([]);
await fetchWhiteListData(field.action);
} else {
await removeRow(code).then((result) => {
showSavedSettingsNotice(result.message);
});
}
}, [removeRow]);
const blockCountryByCode = useCallback(async (code, name) => {
if (Array.isArray(code)) {
//We get all the iso2 codes and names from the array
const ids = code.map(item => ({
iso2_code: item.iso2_code,
country_name: item.country_name
}));
//we loop through the ids and block them one by one
ids.forEach((id) => {
addRow(id.iso2_code, id.country_name).then((result) => {
showSavedSettingsNotice(result.message);
});
});
setRowsSelected([]);
} else {
await addRow(code, name).then((result) => {
showSavedSettingsNotice(result.message);
});
}
}, [addRow]);
const data = {...WhiteListTable.data};
const generateFlag = useCallback((flag, title) => (
<>
<Flag
countryCode={flag}
style={{
fontSize: '2em',
}}
title={title}
/>
</>
), []);
const ActionButton = ({ onClick, children, className }) => (
// <div className={`rsssl-action-buttons__inner`}>
<button
className={`button ${className} rsssl-action-buttons__button`}
onClick={onClick}
disabled={processing}
>
{children}
</button>
// </div>
);
const handleClose = () => {
setModalOpen(false);
}
const handleOpen = () => {
setModalOpen(true);
}
const generateActionButtons = useCallback((id) => {
return (<div className="rsssl-action-buttons">
<ActionButton
onClick={() => allowById(id)} className="button-red">
{__("Reset", "really-simple-ssl")}
</ActionButton>
</div>)
}, [moduleName, allowById, blockRegionByCode, allowRegionByCode, blockCountryByCode, allowCountryByCode]);
for (const key in data) {
const dataItem = {...data[key]};
dataItem.action = generateActionButtons(dataItem.id);
dataItem.flag = generateFlag(dataItem.iso2_code, dataItem.country_name);
dataItem.status = __(dataItem.status = dataItem.status.charAt(0).toUpperCase() + dataItem.status.slice(1), 'really-simple-ssl');
data[key] = dataItem;
}
let paginationSet;
paginationSet = typeof pagination !== 'undefined';
useEffect(() => {
if (Object.keys(data).length === 0 ) {
setTableHeight(100); // Adjust depending on your UI measurements
} else {
setTableHeight(rowHeight * (paginationSet ? pagination.perPage + 2 : 12)); // Adjust depending on your UI measurements
}
}, [paginationSet, pagination?.perPage, data]);
useEffect(() => {
const filteredData = Object.entries(data)
.filter(([_, dataItem]) => {
return Object.values(dataItem).some(val => ((val ?? '').toString().toLowerCase().includes(searchTerm.toLowerCase())));
})
.map(([key, dataItem]) => {
const newItem = { ...dataItem };
newItem.action = generateActionButtons(newItem.id);
newItem.flag = generateFlag(newItem.iso2_code, newItem.country_name);
newItem.status = __(newItem.status = newItem.status.charAt(0).toUpperCase() + newItem.status.slice(1), 'really-simple-ssl');
return [key, newItem];
})
.reduce((obj, [key, val]) => ({ ...obj, [key]: val }), {});
}, [searchTerm, data]);
return (
<>
<TrustIpAddressModal
isOpen={modalOpen}
onRequestClose={handleClose}
value={ipAddress}
status={'trusted'}
>
</TrustIpAddressModal>
<div className="rsssl-container">
{/*display the add button on left side*/}
<AddButton
moduleName={moduleName}
handleOpen={handleOpen}
processing={processing}
blockedText={__("Block IP Address", "really-simple-ssl")}
allowedText={__("Trust IP Address", "really-simple-ssl")}
/>
<div className="rsssl-search-bar">
<div className="rsssl-search-bar__inner">
<div className="rsssl-search-bar__icon"></div>
<input
type="text"
className="rsssl-search-bar__input"
placeholder={__("Search", "really-simple-ssl")}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
</div>
</div>
{rowsSelected.length > 0 && (
<div
style={{
marginTop: '1em',
marginBottom: '1em',
}}
>
<div className={"rsssl-multiselect-datatable-form rsssl-primary"}>
<div>
{__("You have selected %s rows", "really-simple-ssl").replace('%s', rowsSelected.length)}
</div>
<div className="rsssl-action-buttons">
<>
<ActionButton
onClick={() => allowById(rowsSelected)} className="button-red">
{__("Reset", "really-simple-ssl")}
</ActionButton>
</>
</div>
</div>
</div>
)}
{DataTable &&
<DataTable
columns={columns}
data={Object.values(data).filter((row) => {
return Object.values(row).some((val) => ((val ?? '').toString().toLowerCase()).includes(searchTerm.toLowerCase()));
})}
dense
pagination={true}
paginationComponentOptions={{
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
rangeSeparatorText: __('of', 'really-simple-ssl'),
noRowsPerPage: false,
selectAllRowsItem: false,
selectAllRowsItemText: __('All', 'really-simple-ssl'),
}}
noDataComponent={__("No results", "really-simple-ssl")}
persistTableHead
selectableRows={true}
clearSelectedRows={rowCleared}
paginationPerPage={rowsPerPage}
onChangePage={handlePageChange}
onChangeRowsPerPage={handlePerRowsChange}
onSelectedRowsChange={handleSelection}
theme="really-simple-plugins"
customStyles={customStyles}
/> }
{!enabled && (
<div className="rsssl-locked">
<div className="rsssl-locked-overlay"><span
className="rsssl-task-status rsssl-open">{__('Disabled', 'really-simple-ssl')}</span><span>{__('Here you can add IP addresses that should never be blocked by region restrictions.', 'really-simple-ssl')}</span>
</div>
</div>
)}
</>
);
}
export default WhiteListDatatable;

View File

@@ -0,0 +1,202 @@
/* Creates A Store For Risk Data using Zustand */
import {create} from 'zustand';
import * as rsssl_api from "../../utils/api";
const WhiteListTableStore = create((set, get) => ({
processing: false,
processing_block: false,
dataLoaded: false,
dataLoaded_block: false,
pagination: {},
dataActions: {},
WhiteListTable: [],
BlockListData: [],
rowCleared: false,
maskError: false,
ipAddress: '',
note: '',
fetchWhiteListData: async (action) => {
//we check if the processing is already true, if so we return
set({processing: true});
set({dataLoaded: false});
set({rowCleared: true});
try {
const response = await rsssl_api.doAction(
action
);
//now we set the EventLog
if (response && response.request_success) {
set({WhiteListTable: response, dataLoaded: true, processing: false, pagination: response.pagination});
}
set({ rowCleared: true });
} catch (e) {
console.error(e);
} finally {
set({processing: false});
set({rowCleared: false});
}
},
fetchData: async (action, filter) => {
//we check if the processing is already true, if so we return
set({processing_block: true});
set({rowCleared: true});
try {
const response = await rsssl_api.doAction(
action,
{
filterValue: filter
}
);
//now we set the EventLog
if (response && response.request_success) {
set({BlockListData: response, dataLoaded: true, processing: false, pagination: response.pagination});
}
set({ rowCleared: true });
} catch (e) {
console.error(e);
} finally {
set({dataLoaded_block: true})
set({processing_block: false});
set({rowCleared: false});
}
},
resetRow: async (id, dataActions) => {
set({processing: true});
let data = {
id: id
};
try {
const response = await rsssl_api.doAction('geo_block_reset_ip', data);
// Consider checking the response structure for any specific success or failure signals
if (response && response.request_success) {
// Potentially notify the user of success, if needed.
return { success: true, message: response.message, response };
} else {
// Handle any unsuccessful response if needed.
return { success: false, message: response?.message || 'Failed to reset Ip', response };
}
} catch (e) {
console.error(e);
// Notify the user of an error.
return { success: false, message: 'Error occurred', error: e };
} finally {
set({processing: false});
}
}
,
updateRow: async (ip, note, status, filter) => {
set({processing: true});
let data = {
ip_address: ip,
note: note,
status: status
};
try {
const response = await rsssl_api.doAction('geo_block_add_white_list_ip', data);
// Consider checking the response structure for any specific success or failure signals
if (response && response.request_success) {
await get().fetchWhiteListData('rsssl_geo_white_list');
return { success: true, message: response.message, response };
} else {
// Handle any unsuccessful response if needed.
return { success: false, message: response?.message || 'Failed to add Ip', response };
}
} catch (e) {
console.error(e);
// Notify the user of an error.
return { success: false, message: 'Error occurred', error: e };
} finally {
set({processing: false});
}
},
removeRow: async (country, dataActions) => {
set({processing: true});
let data = {
country_code: country
};
try {
const response = await rsssl_api.doAction('geo_block_remove_blocked_country', data);
// Consider checking the response structure for any specific success or failure signals
if (response && response.request_success) {
await get().fetchCountryData('rsssl_geo_white_list');
await get().fetchData('rsssl_geo_block_list', {filterValue: 'all'});
// Potentially notify the user of success, if needed.
return { success: true, message: response.message, response };
} else {
// Handle any unsuccessful response if needed.
return { success: false, message: response?.message || 'Failed to remove country', response };
}
} catch (e) {
console.error(e);
// Notify the user of an error.
return { success: false, message: 'Error occurred', error: e };
} finally {
set({processing: false});
}
},
/*
* This function sets the ip address and is used by Cidr and IpAddressInput
*/
setIpAddress: (ipAddress) => {
if(ipAddress.length === 0) {
return;
}
let ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$|^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,4}|((25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9]))|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9]))$/;
if (ipAddress.includes('/')) {
let finalIp = '';
// Split the input into IP and CIDR mask
let [ip, mask] = ipAddress.split('/');
//if , we change it to .
ip = ip.replace(/,/g, '.');
if (mask.length <= 0 ) {
if (!ipRegex.test(ip)) {
set({maskError: true});
} else {
set({maskError: false});
}
finalIp = `${ip}/${mask}`;
} else {
finalIp = mask ? `${ip}/${mask}` : ip;
}
set({ ipAddress: finalIp })
} else {
if (!ipRegex.test(ipAddress)) {
set({maskError: true});
} else {
set({maskError: false});
}
set({ ipAddress: ipAddress.replace(/,/g, '.') })
}
},
setNote: (note) => {
set({note});
},
resetRange: () => {
set({inputRangeValidated: false});
set({highestIP: ''});
set({lowestIP: ''});
set({ipAddress: ''});
set({maskError: false});
},
setDataLoaded: (dataLoaded) => {
set({dataLoaded});
set({dataLoaded_block: dataLoaded});
}
}));
export default WhiteListTableStore;

View File

@@ -0,0 +1,34 @@
import Icon from "../utils/Icon";
import { __ } from '@wordpress/i18n';
import DOMPurify from "dompurify";
/**
* Render a help notice in the sidebar
*/
const Help = (props) => {
let notice = {...props.help};
if ( !notice.title ){
notice.title = notice.text;
notice.text = false;
}
let openStatus = props.noticesExpanded ? 'open' : '';
//we can use notice.linked_field to create a visual link to the field.
let target = notice.url && notice.url.indexOf("really-simple-ssl.com") !==-1 ? "_blank" : '_self';
return (
<div>
{ notice.title && notice.text &&
<details className={"rsssl-wizard-help-notice rsssl-" + notice.label.toLowerCase()} open={openStatus}>
<summary>{notice.title} <Icon name='chevron-down' /></summary>
{/*some notices contain html, like for the htaccess notices. A title is required for those options, otherwise the text becomes the title. */}
<div dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(notice.text)}}></div>
{notice.url && <div className="rsssl-help-more-info"><a target={target} href={notice.url}>{__("More info", "really-simple-ssl")}</a></div>}
</details>
}
{ notice.title && !notice.text &&
<div className={"rsssl-wizard-help-notice rsssl-" + notice.label.toLowerCase()}><p>{notice.title}</p></div>
}
</div>
);
}
export default Help

View File

@@ -0,0 +1,66 @@
import { useState, useEffect, memo } from "@wordpress/element";
import { ThemeProvider } from '@mui/material/styles';
import useFields from "../FieldsData";
import AutoCompleteControl from "../AutoComplete/AutoCompleteControl";
import useHostData from "./HostData";
import { __ } from "@wordpress/i18n";
import autoCompleteSharedTheme from "../../utils/autoCompleteTheme";
const Host = ({ field, showDisabledWhenSaving = true }) => {
const { updateField, setChangedField, saveFields, handleNextButtonDisabled } = useFields();
const [disabled, setDisabled] = useState(false);
const { fetchHosts, hosts, hostsLoaded } = useHostData();
useEffect(() => {
if (!hostsLoaded) {
fetchHosts();
}
}, []);
useEffect(() => {
handleNextButtonDisabled(disabled);
}, [disabled]);
const onChangeHandler = async (fieldValue) => {
if (showDisabledWhenSaving) {
setDisabled(true);
}
updateField(field.id, fieldValue);
setChangedField(field.id, fieldValue);
await saveFields(true, false);
setDisabled(false);
};
let loadedHosts = hostsLoaded ? hosts : [];
let options = [];
let item = {
label: __('Optional - Select your hosting provider.', 'really-simple-ssl'),
value: '',
};
if (field.value.length === 0) {
options.push(item);
}
for (let key in loadedHosts) {
if (loadedHosts.hasOwnProperty(key)) {
let item = {};
item.label = loadedHosts[key].name;
item.value = key;
options.push(item);
}
}
return (
<ThemeProvider theme={autoCompleteSharedTheme}>
<AutoCompleteControl
className="rsssl-select"
field={field}
label={field.label}
onChange={(fieldValue) => onChangeHandler(fieldValue)}
value={field.value}
options={options}
disabled={disabled}
/>
</ThemeProvider>
);
};
export default memo(Host);

View File

@@ -0,0 +1,25 @@
import {create} from 'zustand';
import * as rsssl_api from "../../utils/api";
const useHostData = create(( set, get ) => ({
hosts: [],
hostsLoaded:false,
fetchHosts: async ( id ) => {
try {
const response = await rsssl_api.doAction('get_hosts', { id: id });
// Handle the response
if ( !response ) {
console.error('No response received from the server.');
return;
}
let hosts = response.hosts;
// Set the roles state with formatted data
set({hosts: hosts,hostsLoaded:true });
} catch (error) {
console.error('Error:', error);
}
}
}));
export default useHostData;

View File

@@ -0,0 +1,12 @@
import { __ } from '@wordpress/i18n';
import useLearningMode from "./LearningModeData";
const ChangeStatus = (props) => {
const {updateStatus} = useLearningMode();
let statusClass = props.item.status==1 ? 'button button-primary rsssl-status-allowed' : 'button button-default rsssl-status-revoked';
let label = props.item.status==1 ? __("Revoke", "really-simple-ssl") : __("Allow", "really-simple-ssl");
return (
<button onClick={ () => updateStatus( props.item.status, props.item, props.field.id ) } className={statusClass}>{label}</button>
)
}
export default ChangeStatus

View File

@@ -0,0 +1,16 @@
import useLearningMode from "./LearningModeData";
import {__} from "@wordpress/i18n";
const Delete = (props) => {
const {deleteData} = useLearningMode();
return (
<button type="button" className="button button-red rsssl-learning-mode-delete" onClick={ () => deleteData( props.item, props.field.id ) }>
{/*<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" height="16" >*/}
{/* <path fill="#000000" d="M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z"/>*/}
{/*</svg>*/}
{__("Delete", "really-simple-ssl")}
</button>
)
}
export default Delete

View File

@@ -0,0 +1,400 @@
import { __ } from '@wordpress/i18n';
import {useState,useEffect} from '@wordpress/element';
import ChangeStatus from "./ChangeStatus";
import Delete from "./Delete";
import Icon from "../../utils/Icon";
import useFields from "./../FieldsData";
import UseLearningMode from "./LearningModeData";
import {Button} from "@wordpress/components";
import React from "react";
import ManualCspAdditionModal from "./ManualCspAdditionModal";
import AddButton from "../GeoBlockList/AddButton";
import ManualCspAddition from "./ManualCspAddition";
const LearningMode = (props) => {
const {updateField, getFieldValue, getField, setChangedField, highLightField, saveFields} = useFields();
const {fetchLearningModeData, learningModeData, dataLoaded, updateStatus, deleteData} = UseLearningMode();
const {manualAdditionProcessing} = ManualCspAddition();
//used to show if a feature is already enforced by a third party
const [enforcedByThirdparty, setEnforcedByThirdparty] = useState(0);
//toggle from enforced to not enforced
const [enforce, setEnforce] = useState(0);
//toggle from learning mode to not learning mode
const [learningMode, setLearningMode] = useState(0);
//set learning mode to completed
const [learningModeCompleted, setLearningModeCompleted] = useState(0);
const [hasError, setHasError] = useState(false);
//check if learningmode has been enabled at least once
const [lmEnabledOnce, setLmEnabledOnce] = useState(0);
//filter the data
const [filterValue, setFilterValue] = useState(-1);
//the value that is used to enable or disable this feature. On or of.
const [controlField, setControlField] = useState(false);
// the value that is used to select and deselect rows
const [rowsSelected, setRowsSelected] = useState([]);
const [rowCleared, setRowCleared] = useState(false);
const [DataTable, setDataTable] = useState(null);
const [theme, setTheme] = useState(null);
const [modalOpen, setModalOpen] = useState(false);
useEffect( () => {
import('react-data-table-component').then(({ default: DataTable, createTheme }) => {
setDataTable(() => DataTable);
setTheme(() => createTheme('really-simple-plugins', {
divider: {
default: 'transparent',
},
}, 'light'));
});
}, []);
/**
* Styling
*/
const conditionalRowStyles = [
{
when: row => row.status ==0,
classNames: ['rsssl-datatables-revoked'],
},
];
const customStyles = {
headCells: {
style: {
paddingLeft: '0', // override the cell padding for head cells
paddingRight: '0',
},
},
cells: {
style: {
paddingLeft: '0', // override the cell padding for data cells
paddingRight: '0',
},
},
};
;
/**
* Initialize
*/
useEffect(() => {
const run = async () => {
await fetchLearningModeData(props.field.id);
let controlField = getField(props.field.control_field );
let enforced_by_thirdparty = controlField.value === 'enforced-by-thirdparty';
let enforce = enforced_by_thirdparty || controlField.value === 'enforce';
setControlField(controlField);
setEnforcedByThirdparty(enforced_by_thirdparty);
setLearningModeCompleted(controlField.value==='completed');
setHasError(controlField.value==='error');
setLmEnabledOnce(getFieldValue(props.field.control_field+'_lm_enabled_once'))
setEnforce(enforce);
setLearningMode(controlField.value === 'learning_mode');
}
run();
}, [enforce, learningMode] );
const toggleEnforce = async (e, enforceValue) => {
e.preventDefault();
//enforce this setting
let controlFieldValue = enforceValue==1 ? 'enforce' : 'disabled';
setEnforce(enforceValue);
setLearningModeCompleted(0);
setLearningMode(0);
setChangedField(controlField.id, controlFieldValue);
updateField(controlField.id, controlFieldValue);
await saveFields(true, false);
//await fetchLearningModeData();
}
const toggleLearningMode = async (e) => {
e.preventDefault();
let lmEnabledOnceField = getField(props.field.control_field+'_lm_enabled_once');
if ( learningMode ) {
setLmEnabledOnce(1);
updateField(lmEnabledOnceField.id, 1);
}
let controlFieldValue;
if ( learningMode || learningModeCompleted ) {
setLearningMode(0);
controlFieldValue = 'disabled';
} else {
setLearningMode(1);
controlFieldValue = 'learning_mode';
}
setLearningModeCompleted(0);
setChangedField(controlField.id, controlFieldValue);
updateField(controlField.id, controlFieldValue);
setChangedField(lmEnabledOnceField.id, lmEnabledOnceField.value);
updateField(lmEnabledOnceField, lmEnabledOnceField.value);
await saveFields(true, false);
}
const Filter = () => (
<>
<select onChange={ ( e ) => setFilterValue(e.target.value) } value={filterValue}>
<option value="-1" >{__("All", "really-simple-ssl")}</option>
<option value="1" >{__("Allowed", "really-simple-ssl")}</option>
<option value="0" >{__("Blocked", "really-simple-ssl")}</option>
</select>
</>
);
let field = props.field;
let configuringString = __(" The %s is now in report-only mode and will collect directives. This might take a while. Afterwards you can Exit, Edit and Enforce these Directives.", "really-simple-ssl").replace('%s', field.label);
let disabledString = __("%s has been disabled.", "really-simple-ssl").replace('%s', field.label);
let enforcedString = __("%s is enforced.", "really-simple-ssl").replace('%s', field.label);
let enforceDisabled = !lmEnabledOnce;
if (enforcedByThirdparty) disabledString = __("%s is already set outside Really Simple Security.", "really-simple-ssl").replace('%s', field.label);
let highLightClass = 'rsssl-field-wrap';
if ( highLightField===props.field.id ) {
highLightClass = 'rsssl-field-wrap rsssl-highlight';
}
//build our header
let columns = [];
field.columns.forEach(function(item, i) {
let newItem = {
name: item.name,
sortable: item.sortable,
width: item.width,
selector: item.column === 'documenturi' || item.column === 'method'
? row => <span title={row[item.column]}>{row[item.column]}</span>: row => row[item.column],
}
columns.push(newItem);
});
let data = learningModeData;
data = data.filter(item => item.status<2);
if (filterValue!=-1) {
data = data.filter(item => item.status==filterValue);
}
for (const item of data){
if (item.login_status) item.login_statusControl = item.login_status == 1 ? __("success", "really-simple-ssl") : __("failed", "really-simple-ssl");
item.statusControl = <ChangeStatus item={item} field={props.field} />;
item.deleteControl = <Delete item={item} field={props.field}/>;
item.grouped = <div className="rsssl-action-buttons">
<ChangeStatus item={item} field={props.field} />
<Delete item={item} field={props.field}/>
</div>
}
const handleMultiRowStatus = (status, selectedRows, type) => {
selectedRows.forEach(row => {
//the updateItemId allows us to update one specific item in a field set.
updateStatus(status, row, type);
});
setRowCleared(true);
setRowsSelected([]);
// Reset rowCleared back to false after the DataTable has re-rendered
setTimeout(() => setRowCleared(false), 0);
}
const handleMultiRowDelete = ( selectedRows, type) => {
selectedRows.forEach(row => {
//the updateItemId allows us to update one specific item in a field set.
deleteData( row, type );
});
setRowCleared(true);
setRowsSelected([]);
// Reset rowCleared back to false after the DataTable has re-rendered
setTimeout(() => setRowCleared(false), 0);
}
function handleSelection(state) {
setRowCleared(false);
setRowsSelected(state.selectedRows);
}
const handleClose = () => {
setModalOpen(false);
}
const handleOpen = () => {
setModalOpen(true);
}
if (!DataTable || !theme) return null;
return (
<>
{props.field.id === 'content_security_policy_source_directives' && (<>
<ManualCspAdditionModal
isOpen={modalOpen}
onRequestClose={handleClose}
status={'blocked'}
directives={props.field.modal.options}
parentId={props.field.id}
/>
<div className="rsssl-container" style={{paddingTop: "0px"}}>
<AddButton
handleOpen={handleOpen}
processing={manualAdditionProcessing}
allowedText={__("Add Entry", "really-simple-ssl")}
/>
</div>
</>)}
{!dataLoaded && <>
<div className="rsssl-learningmode-placeholder">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</>}
{rowsSelected.length > 0 && (
<div
style={{
marginTop: '1em',
marginBottom: '1em',
}}
>
<div
className={"rsssl-multiselect-datatable-form rsssl-primary"}
>
<div>
{__("You have selected", "really-simple-ssl")} {rowsSelected.length} {__("rows", "really-simple-ssl")}
</div>
<div className="rsssl-action-buttons">
{(Number(filterValue) === -1 || Number(filterValue) === 0) &&
<div className="rsssl-action-buttons__inner">
<Button
// className={"button button-red rsssl-action-buttons__button"}
className={"button button-secondary rsssl-status-allowed rsssl-action-buttons__button"}
onClick={() => handleMultiRowStatus(0, rowsSelected, props.field.id)}
>
{__('Allow', 'really-simple-ssl')}
</Button>
</div>
}
{(Number(filterValue) === -1 || Number(filterValue) === 1) &&
<div className="rsssl-action-buttons__inner">
<Button
// className={"button button-red rsssl-action-buttons__button"}
className={"button button-primary rsssl-action-buttons__button"}
onClick={() => handleMultiRowStatus(1, rowsSelected, props.field.id)}
>
{__('Revoke', 'really-simple-ssl')}
</Button>
</div>
}
<div className="rsssl-action-buttons__inner">
<Button
// className={"button button-red rsssl-action-buttons__button"}
className={"button button-red rsssl-action-buttons__button"}
onClick={() => handleMultiRowDelete(rowsSelected, props.field.id)}
>
{__('Remove', 'really-simple-ssl')}
</Button>
</div>
</div>
</div>
</div>
)}
{dataLoaded && <>
<DataTable
columns={columns}
data={data}
dense
pagination
noDataComponent={__("No results", "really-simple-ssl")}
persistTableHead
theme={theme}
customStyles={customStyles}
conditionalRowStyles={conditionalRowStyles}
paginationComponentOptions={{
rowsPerPageText: __('Rows per page:', 'really-simple-ssl'),
rangeSeparatorText: __('of', 'really-simple-ssl'),
noRowsPerPage: false,
selectAllRowsItem: false,
selectAllRowsItemText: __('All', 'really-simple-ssl'),
}}
selectableRows
selectableRowsHighlight={true}
onSelectedRowsChange={handleSelection}
clearSelectedRows={rowCleared}
/></>
}
<div className={"rsssl-learning-mode-footer"} style={{marginLeft:'0px'}}>
{hasError && <div className="rsssl-locked">
<div className="rsssl-locked-overlay">
<span
className="rsssl-progress-status rsssl-learning-mode-error">{__("Error detected", "really-simple-ssl")}</span>
{__("%s cannot be implemented due to server limitations. Check your notices for the detected issue.", "really-simple-ssl").replace('%s', field.label)}&nbsp;
<a className="rsssl-learning-mode-link" href="#"
onClick={(e) => toggleEnforce(e, false)}>{__("Disable", "really-simple-ssl")}</a>
</div>
</div>
}
{!hasError && <>
{enforce != 1 && <button disabled={enforceDisabled} className="button button-primary"
onClick={(e) => toggleEnforce(e, true)}>{__("Enforce", "really-simple-ssl")}</button>}
{!enforcedByThirdparty && enforce == 1 && <button className="button"
onClick={(e) => toggleEnforce(e, false)}>{__("Disable", "really-simple-ssl")}</button>}
<label>
<input type="checkbox"
disabled={enforce}
checked={learningMode == 1}
value={learningMode}
onChange={(e) => toggleLearningMode(e)}
/>
{__("Enable Learning Mode to configure automatically", "really-simple-ssl")}
</label>
{enforce == 1 && <div className="rsssl-locked">
<div className="rsssl-shield-overlay">
<Icon name="shield" size="80px"/>
</div>
<div className="rsssl-locked-overlay">
<span
className="rsssl-progress-status rsssl-learning-mode-enforced">{__("Enforced", "really-simple-ssl")}</span>
{enforcedString}&nbsp;
<a className="rsssl-learning-mode-link" href="#"
onClick={(e) => toggleEnforce(e)}>{__("Disable to configure", "really-simple-ssl")}</a>
</div>
</div>}
{learningMode == 1 && <div className="rsssl-locked">
<div className="rsssl-locked-overlay">
<span
className="rsssl-progress-status rsssl-learning-mode">{__("Learning Mode", "really-simple-ssl")}</span>
{configuringString}&nbsp;
<a className="rsssl-learning-mode-link" href="#"
onClick={(e) => toggleLearningMode(e)}>{__("Exit", "really-simple-ssl")}</a>
</div>
</div>}
{learningModeCompleted == 1 && <div className="rsssl-locked">
<div className="rsssl-locked-overlay">
<span
className="rsssl-progress-status rsssl-learning-mode-completed">{__("Learning Mode", "really-simple-ssl")}</span>
{__("We finished the configuration.", "really-simple-ssl")}&nbsp;
<a className="rsssl-learning-mode-link" href="#"
onClick={(e) => toggleLearningMode(e)}>{__("Review the settings and enforce the policy", "really-simple-ssl")}</a>
</div>
</div>}
{rsssl_settings.pro_plugin_active && props.disabled && <div className="rsssl-locked ">
<div className="rsssl-locked-overlay">
{!enforcedByThirdparty && <span
className="rsssl-progress-status rsssl-disabled">{__("Disabled", "really-simple-ssl")}</span>}
{enforcedByThirdparty && <span
className="rsssl-progress-status rsssl-learning-mode-enforced">{__("Enforced", "really-simple-ssl")}</span>}
{disabledString}
</div>
</div>}
</>
}
<Filter/>
</div>
</>
)
}
export default LearningMode

Some files were not shown because too many files have changed in this diff Show More