clean build for version 1.4.5 with fixes of security funtionalities, logic branches, etc. Already tested and working fine

This commit is contained in:
dwindown
2026-01-07 15:10:47 +07:00
parent 31b3398c2f
commit 0ba62b435a
26 changed files with 8962 additions and 2804 deletions

View File

@@ -0,0 +1,213 @@
<!-- Handlebars Template for Fields -->
<script id="fields-template" type="text/x-handlebars-template">
{{#each fields}}
<div class="dw-checker-field">
<label for="{{fieldId}}" style="color: {{fieldLabelColor}}; display: {{fieldDisplayLabel}};">{{fieldLabel}}</label>
{{#if isTextField}}
<input name="{{fieldId}}" placeholder="{{fieldPlaceholder}}"/>
{{else if isSelectField}}
<select name="{{fieldId}}" placeholder="{{fieldPlaceholder}}">
<option value="" selected disabled>-- {{fieldPlaceholder}} --</option>
{{#each uniqueValues}}
<option value="{{this}}">{{this}}</option>
{{/each}}
</select>
{{/if}}
</div>
{{/each}}
</script>
<!-- Vertical Table Template -->
<script id="vertical-table-template" type="text/x-handlebars-template">
<div class="dw-checker-results-container">
{{#each results}}
<div class="result-page" data-page="{{@index}}">
<table class="dw-checker-result-table" {{{getStyle ../resultDivider ../resultDividerWidth}}}>
<tbody>
{{#each this}}
{{#unless (getColumnSetting @key 'hide')}}
<tr>
<th {{{getStyleHeader ../resultDivider ../resultDividerWidth ../headerColor}}}>
<span class="dw-checker-result-header">{{@key}}</span>
</th>
<td {{{getStyleValue ../resultDivider ../resultDividerWidth ../valueColor}}}>
{{#if (eq (getColumnSetting @key 'type') 'link_button')}}
<span class="dw-checker-result-value">{{getColumnSetting @key 'prefix' this}} <a href="{{getValueWithPrefix @key}}" class="btn btn-primary">{{getColumnSetting @key 'button_text'}}</a></span>
{{else if (eq (getColumnSetting @key 'type') 'whatsapp_button')}}
<span class="dw-checker-result-value">{{getColumnSetting @key 'prefix' this}} <a href="https://wa.me/{{getValueWithPrefix @key}}" class="btn btn-success">{{getColumnSetting @key 'button_text'}}</a></span>
{{else if (eq (getColumnSetting @key 'type') 'text')}}
<!-- Raw value for text type -->
<span class="dw-checker-result-value">{{getColumnSetting @key 'prefix' this}} {{formatValue this}}</span>
{{else}}
<!-- Default behavior: raw value -->
<span class="dw-checker-result-value">{{getColumnSetting @key 'prefix' this}} {{formatValue this}}</span>
{{/if}}
</td>
</tr>
{{/unless}}
{{/each}}
</tbody>
</table>
</div>
{{/each}}
</div>
<div class="pagination-controls">
<button type="button" class="prev-page">Previous</button>
<span class="current-page">Data 1</span>
<button type="button" class="next-page">Next</button>
</div>
</script>
<!-- Div Template -->
<script id="div-template" type="text/x-handlebars-template">
<div class="dw-checker-results-container">
{{#each results}}
<div class="result-page" data-page="{{@index}}">
<div class="dw-checker-result-div" {{{getStyle ../resultDivider ../resultDividerWidth}}}>
{{#each this}}
{{#unless (getColumnSetting @key 'hide')}}
<div class="dw-checker-result-div-item" {{{getStyleHeader ../resultDivider ../resultDividerWidth ../headerColor}}}>
<div class="result-header" {{{getStyleHeader ../resultDivider ../resultDividerWidth ../headerColor}}}>
<span class="dw-checker-result-header">{{@key}}</span>
</div>
<div class="result-value" {{{getStyleValue ../resultDivider ../resultDividerWidth ../valueColor}}}>
{{#if (eq (getColumnSetting @key 'type') 'link_button')}}
<span class="dw-checker-result-value">{{getColumnSetting @key 'prefix' this}} <a href="{{getValueWithPrefix @key}}" class="btn btn-primary">{{getColumnSetting @key 'button_text'}}</a></span>
{{else if (eq (getColumnSetting @key 'type') 'whatsapp_button')}}
<span class="dw-checker-result-value">{{getColumnSetting @key 'prefix' this}} <a href="https://wa.me/{{getValueWithPrefix @key}}" class="btn btn-success">{{getColumnSetting @key 'button_text'}}</a></span>
{{else if (eq (getColumnSetting @key 'type') 'text')}}
<!-- Raw value for text type -->
<span class="dw-checker-result-value">{{getColumnSetting @key 'prefix' this}} {{formatValue this}}</span>
{{else}}
<!-- Default behavior: raw value -->
<span class="dw-checker-result-value">{{getColumnSetting @key 'prefix' this}} {{formatValue this}}</span>
{{/if}}
</div>
</div>
{{/unless}}
{{/each}}
</div>
</div>
{{/each}}
</div>
<div class="pagination-controls">
<button type="button" class="prev-page">Previous</button>
<span class="current-page">Data 1</span>
<button type="button" class="next-page">Next</button>
</div>
</script>
<!-- Card Template -->
<script id="cards-template" type="text/x-handlebars-template">
<div class="dw-cards-container">
{{#each results}}
<div class="result-page" data-page="{{@index}}" style="display: none;">
{{#each this}}
<div class="dw-card">
<div class="dw-card-item">
<h6 class="dw-card-title">{{@key}}</h6>
<p class="dw-card-value">
{{#if (eq (getColumnSetting @key 'type' this) 'link_button')}}
<span class="dw-checker-result-value">{{getColumnSetting @key 'prefix' this}} <a href="{{getValueWithPrefix @key}}" class="btn btn-primary">{{getColumnSetting @key 'button_text'}}</a></span>
{{else if (eq (getColumnSetting @key 'type' this) 'whatsapp_button')}}
<span class="dw-checker-result-value">{{getColumnSetting @key 'prefix' this}} <a href="https://wa.me/{{getValueWithPrefix @key}}" class="btn btn-success">{{getColumnSetting @key 'button_text' this}}</a></span>
{{else if (eq (getColumnSetting @key 'type' this) 'text')}}
<!-- Raw value for text type -->
<span class="dw-checker-result-value">{{getColumnSetting @key 'prefix' this}} {{formatValue this}}</span>
{{else}}
<!-- Default behavior: raw value -->
<span class="dw-checker-result-value">{{getColumnSetting @key 'prefix' this}} {{formatValue this}}</span>
{{/if}}
</p>
</div>
</div>
{{/each}}
</div>
{{/each}}
</div>
<div class="pagination-controls">
<button type="button" class="prev-page">Previous</button>
<span class="current-page">Data 1</span>
<button type="button" class="next-page">Next</button>
</div>
</script>
<!-- General Table Template -->
<script id="standard-table-template" type="text/x-handlebars-template">
<table class="dw-standard-table table">
<thead>
<tr>
{{#each columnHeaders}}
<th>{{this}}</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each results}}
<tr>
{{#each this}}
<td>{{formatValue this}}</td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
</script>
<script id="repeater-template" type="text/x-handlebars-template">
{{#each fields}}
<div class="card shadow repeater-card gap-2 position-relative">
<div class="card-body">
<div class="row mb-2">
<div class="col-3"><label class="form-label fw-bold mb-0">Field ID</label></div>
<div class="col-9">
<input class="form-control field-id" value="{{@key}}" />
</div>
</div>
<div class="row mb-2">
<div class="col-3"><label class="form-label fw-bold mb-0">Kolom</label></div>
<div class="col-9">
<select name="checker[fields][{{@key}}][kolom]" class="form-select form-control border select-kolom">
{{#each kolom}}
<option value="{{this}}" {{#ifCond ../selected_kolom '==' this}}selected{{/ifCond}}>{{this}}</option>
{{/each}}
</select>
</div>
</div>
<div class="row mb-2">
<div class="col-3"><label class="form-label fw-bold mb-0">Tipe</label></div>
<div class="col-9">
<select name="checker[fields][{{@key}}][type]" class="form-select form-control border select-field-type">
<option value="text" {{#ifCond type '==' 'text'}}selected{{/ifCond}}>Text</option>
<option value="select" {{#ifCond type '==' 'select'}}selected{{/ifCond}}>Select</option>
</select>
</div>
</div>
<div class="row mb-2">
<div class="col-3"><label class="form-label fw-bold mb-0">Label</label></div>
<div class="col-9">
<input name="checker[fields][{{@key}}][label]" class="form-control field-label" value="{{label}}" />
</div>
</div>
<div class="row mb-2">
<div class="col-3"><label class="form-label fw-bold mb-0">Placeholder</label></div>
<div class="col-9">
<input name="checker[fields][{{@key}}][placeholder]" class="form-control field-placeholder" value="{{placeholder}}" />
</div>
</div>
<div class="row mb-2">
<div class="col-3"><label class="form-label fw-bold mb-0">Value Matcher</label></div>
<div class="col-9">
<select name="checker[fields][{{@key}}][match]" class="form-select form-control border select-match-type">
<option value="match" {{#ifCond match '==' 'match'}}selected{{/ifCond}}>Match</option>
<option value="contain" {{#ifCond match '==' 'contain'}}selected{{/ifCond}}>Contain</option>
</select>
</div>
</div>
<div class="card-buttons d-flex gap-2 flex-column position-absolute">
<button type="button" class="btn btn-secondary py-1 px-2 move-card"><i class="bi bi-arrow-down-up"></i></button>
<button type="button" class="btn btn-danger py-1 px-2 delete-form-card"><i class="bi bi-x"></i></button>
</div>
</div>
</div>
{{/each}}
</script>

View File

@@ -168,7 +168,7 @@
<div class="col-9">
<select name="checker[fields][{{@key}}][kolom]" class="form-select form-control border select-kolom">
{{#each kolom}}
<option value="{{this}}">{{this}}</option>
<option value="{{this}}" {{#ifCond ../selected_kolom '==' this}}selected{{/ifCond}}>{{this}}</option>
{{/each}}
</select>
</div>

View File

@@ -27,12 +27,125 @@
<div class="dw-checker-container" id="dw-checker-outside-results" style="display: none;">
<div class="dw-checker-wrapper"></div>
</div>
<input type="hidden" id="post_id" value="<?= (isset($_GET['post']) && isset($_GET['action']) && $_GET['action'] == 'edit') ? $_GET['post'] : '' ?>">
<input type="hidden" id="post_id" value="<?= isset($_GET["post"]) &&
isset($_GET["action"]) &&
$_GET["action"] == "edit"
? $_GET["post"]
: "" ?>">
</div>
<hr>
<div class="input-group mt-3">
<span class="input-group-text" id="basic-addon2">Reset Preview Interval</span>
<input type="number" id="preview-interval" class="form-control border text-end pe-2" aria-describedby="basic-addon2" value="10">
<span class="input-group-text border" id="basic-addon2">seconds</span>
<button class="btn btn-primary border-primary set-preview"><i class="bi bi-arrow-clockwise me-1"></i>Refresh</button>
</div>
<!-- Essential Handlebars templates for preview functionality -->
<!-- Handlebars Template for Fields -->
<script id="fields-template" type="text/x-handlebars-template">
{{#each fields}}
<div class="dw-checker-field">
<label for="{{fieldId}}" style="color: {{fieldLabelColor}}; display: {{fieldDisplayLabel}};">{{fieldLabel}}</label>
{{#if isTextField}}
<input name="{{fieldId}}" placeholder="{{fieldPlaceholder}}"/>
{{else if isSelectField}}
<select name="{{fieldId}}" placeholder="{{fieldPlaceholder}}">
<option value="" selected disabled>-- {{fieldPlaceholder}} --</option>
{{#each uniqueValues}}
<option value="{{this}}">{{this}}</option>
{{/each}}
</select>
{{/if}}
</div>
{{/each}}
</script>
<script id="vertical-table-template" type="text/x-handlebars-template">
<div class="dw-checker-results-container">
{{#each results}}
<div class="result-page" data-page="{{@index}}">
<table class="dw-checker-result-table">
<tbody>
{{#each this}}
<tr>
<th><span class="dw-checker-result-header">{{@key}}</span></th>
<td><span class="dw-checker-result-value">{{this}}</span></td>
</tr>
{{/each}}
</tbody>
</table>
</div>
{{/each}}
</div>
</script>
<script id="div-template" type="text/x-handlebars-template">
<div class="dw-checker-results-container">
{{#each results}}
<div class="result-page" data-page="{{@index}}">
<div class="dw-checker-result-container" data-pagination="{{@index}}">
{{#each this}}
<div class="dw-checker-result-div">
<div class="result-header">
<span class="dw-checker-result-header">{{@key}}</span>
</div>
<div class="result-value">
<span class="dw-checker-result-value">{{this}}</span>
</div>
</div>
{{/each}}
</div>
</div>
{{/each}}
</div>
</script>
<script id="standard-table-template" type="text/x-handlebars-template">
<div class="dw-checker-results-container">
<table class="dw-checker-result-table">
<thead>
{{#if results.[0]}}
<tr>
{{#each results.[0]}}
<th><span class="dw-checker-result-header">{{@key}}</span></th>
{{/each}}
</tr>
{{/if}}
</thead>
<tbody>
{{#each results}}
<tr>
{{#each this}}
<td><span class="dw-checker-result-value">{{this}}</span></td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
</div>
</script>
<!-- Card Template -->
<script id="cards-template" type="text/x-handlebars-template">
<div class="dw-cards-container">
{{#each results}}
<div class="result-page" data-page="{{@index}}" style="display: none;">
{{#each this}}
<div class="dw-card">
<div class="dw-card-item">
<h6 class="dw-card-title">{{@key}}</h6>
<p class="dw-card-value">{{this}}</p>
</div>
</div>
{{/each}}
</div>
{{/each}}
</div>
<div class="pagination-controls">
<button type="button" class="prev-page">Previous</button>
<span class="current-page">Data 1</span>
<button type="button" class="next-page">Next</button>
</div>
<hr>
<div class="input-group mt-3">
<span class="input-group-text" id="basic-addon2">Reset Preview Interval</span>
<input type="number" id="preview-interval" class="form-control border text-end pe-2" aria-describedby="basic-addon2" value="10">
<span class="input-group-text border" id="basic-addon2">seconds</span>
<button class="btn btn-primary border-primary set-preview"><i class="bi bi-arrow-clockwise me-1"></i>Refresh</button>
</div>
</script>

View File

@@ -31,6 +31,16 @@
<small class="text-muted">Maximum records to display (performance limit)</small>
</div>
</div>
<div class="row mb-2">
<div class="col-3"><label class="form-label fw-bold mb-0">Cache Control</label></div>
<div class="col-9">
<button type="button" class="btn btn-sm btn-outline-warning" id="clear-checker-cache" data-checker-id="<?= $post_id ?>">
<i class="bi bi-arrow-clockwise"></i> Clear Cache & Refresh Data
</button>
<small class="text-muted d-block mt-1">Clear cached data to fetch fresh data from Google Sheet (cached for 5 minutes)</small>
<div id="cache-clear-message" class="mt-2" style="display:none;"></div>
</div>
</div>
</td>
</tr>
<tr class="has-link" style="display: none;">
@@ -150,4 +160,49 @@
</td>
</tr>
</tbody>
</table>
</table>
<script>
jQuery(document).ready(function($) {
$('#clear-checker-cache').on('click', function() {
var btn = $(this);
var checkerId = btn.data('checkerId');
var messageDiv = $('#cache-clear-message');
btn.prop('disabled', true).html('<i class="bi bi-arrow-clockwise spinner-border spinner-border-sm"></i> Clearing...');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'checker_clear_cache',
checker_id: checkerId,
security: '<?php echo wp_create_nonce("checker_ajax_nonce"); ?>'
},
success: function(response) {
if (response.success) {
messageDiv.removeClass('alert-danger').addClass('alert alert-success')
.html('<i class="bi bi-check-circle"></i> ' + response.data.message)
.fadeIn();
} else {
messageDiv.removeClass('alert-success').addClass('alert alert-danger')
.html('<i class="bi bi-exclamation-circle"></i> ' + response.data.message)
.fadeIn();
}
setTimeout(function() {
messageDiv.fadeOut();
}, 3000);
},
error: function() {
messageDiv.removeClass('alert-success').addClass('alert alert-danger')
.html('<i class="bi bi-exclamation-circle"></i> Failed to clear cache')
.fadeIn();
},
complete: function() {
btn.prop('disabled', false).html('<i class="bi bi-arrow-clockwise"></i> Clear Cache & Refresh Data');
}
});
});
});
</script>

View File

@@ -3,120 +3,407 @@
<tr class="has-link" style="display: none;">
<th>Rate Limiting</th>
<td>
<p class="text-muted small mb-3">Limit the number of searches per IP address to prevent abuse</p>
<p class="text-muted small mb-3">Limit the number of searches per IP address to prevent abuse and bot attacks</p>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="yes" id="security-rate-limit-enabled" name="checker[security][rate_limit][enabled]" <?= isset($checker['security']['rate_limit']['enabled']) && $checker['security']['rate_limit']['enabled'] == 'yes' ? 'checked' : '' ?>>
<input class="form-check-input" type="checkbox" value="yes" id="security-rate-limit-enabled" name="checker[security][rate_limit][enabled]" <?= isset(
$checker["security"]["rate_limit"]["enabled"],
) && $checker["security"]["rate_limit"]["enabled"] == "yes"
? "checked"
: "" ?>>
<label class="form-check-label fw-bold" for="security-rate-limit-enabled">
Enable Rate Limiting
</label>
</div>
<div class="rate-limit-settings" style="<?= isset($checker['security']['rate_limit']['enabled']) && $checker['security']['rate_limit']['enabled'] == 'yes' ? '' : 'display:none;' ?>">
<div class="rate-limit-settings" style="<?= isset(
$checker["security"]["rate_limit"]["enabled"],
) && $checker["security"]["rate_limit"]["enabled"] == "yes"
? ""
: "display:none;" ?>">
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Max Attempts</label>
<input type="number" name="checker[security][rate_limit][max_attempts]" value="<?= $checker['security']['rate_limit']['max_attempts'] ?? 5 ?>" class="form-control" min="1" max="100">
<small class="text-muted">Maximum searches allowed per time window</small>
<input type="number" name="checker[security][rate_limit][max_attempts]" value="<?= $checker[
"security"
]["rate_limit"]["max_attempts"] ??
5 ?>" class="form-control" min="1" max="100">
<small class="text-muted">Maximum searches allowed per time window (1-100)</small>
</div>
<div class="col-md-6">
<label class="form-label">Time Window (minutes)</label>
<input type="number" name="checker[security][rate_limit][time_window]" value="<?= $checker['security']['rate_limit']['time_window'] ?? 15 ?>" class="form-control" min="1" max="1440">
<small class="text-muted">Reset attempts after this duration</small>
<input type="number" name="checker[security][rate_limit][time_window]" value="<?= $checker[
"security"
]["rate_limit"]["time_window"] ??
15 ?>" class="form-control" min="1" max="1440">
<small class="text-muted">Reset attempts after this duration (1-1440 minutes)</small>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Block Duration (minutes)</label>
<input type="number" name="checker[security][rate_limit][block_duration]" value="<?= $checker['security']['rate_limit']['block_duration'] ?? 60 ?>" class="form-control" min="1" max="10080">
<small class="text-muted">How long to block after exceeding limit</small>
<input type="number" name="checker[security][rate_limit][block_duration]" value="<?= $checker[
"security"
]["rate_limit"]["block_duration"] ??
60 ?>" class="form-control" min="1" max="10080">
<small class="text-muted">How long to block after exceeding limit (1-10080 minutes)</small>
</div>
<div class="col-md-6">
<label class="form-label">Error Message</label>
<input type="text" name="checker[security][rate_limit][error_message]" value="<?= $checker['security']['rate_limit']['error_message'] ?? 'Too many attempts. Please try again later.' ?>" class="form-control">
<input type="text" name="checker[security][rate_limit][error_message]" value="<?= $checker[
"security"
]["rate_limit"]["error_message"] ??
"Too many attempts. Please try again later." ?>" class="form-control">
<small class="text-muted">Message shown when blocked</small>
</div>
</div>
<div class="row mb-3">
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="yes" id="security-rate-limit-whitelist" name="checker[security][rate_limit][whitelist_enabled]" <?= isset(
$checker["security"]["rate_limit"][
"whitelist_enabled"
],
) &&
$checker["security"]["rate_limit"][
"whitelist_enabled"
] == "yes"
? "checked"
: "" ?>>
<label class="form-check-label" for="security-rate-limit-whitelist">
Enable IP Whitelist
</label>
</div>
<div id="whitelist-ips" style="<?= isset(
$checker["security"]["rate_limit"][
"whitelist_enabled"
],
) &&
$checker["security"]["rate_limit"][
"whitelist_enabled"
] == "yes"
? ""
: "display:none;" ?>" class="mt-3">
<label class="form-label">Whitelisted IPs (one per line)</label>
<textarea name="checker[security][rate_limit][whitelist_ips]" class="form-control" rows="3"><?= $checker[
"security"
]["rate_limit"]["whitelist_ips"] ??
"" ?></textarea>
<small class="text-muted">IPs that bypass rate limiting (supports CIDR notation like 192.168.1.0/24)</small>
</div>
</div>
</div>
</div>
</td>
</tr>
<tr class="has-link" style="display: none;">
<th>Google reCAPTCHA v3</th>
<td>
<p class="text-muted small mb-3">Invisible CAPTCHA protection. <a href="https://www.google.com/recaptcha/admin" target="_blank">Get keys here</a></p>
<div class="alert alert-info small mb-3">
<strong>How to get keys:</strong><br>
1. Go to <a href="https://www.google.com/recaptcha/admin/create" target="_blank" rel="noopener">reCAPTCHA Admin Console</a><br>
2. Create a new site with <strong>Score based (v3)</strong> type<br>
3. Add your domain (e.g., dwindi.com)<br>
4. Copy the <strong>Site Key</strong> and <strong>Secret Key</strong> shown after creation<br>
<em class="text-muted">Note: If using Google Cloud Console, click "Use Legacy Key" under Integration tab to get the secret key</em>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="yes" id="security-recaptcha-enabled" name="checker[security][recaptcha][enabled]" <?= isset($checker['security']['recaptcha']['enabled']) && $checker['security']['recaptcha']['enabled'] == 'yes' ? 'checked' : '' ?>>
<input class="form-check-input" type="checkbox" value="yes" id="security-recaptcha-enabled" name="checker[security][recaptcha][enabled]" <?= isset(
$checker["security"]["recaptcha"]["enabled"],
) && $checker["security"]["recaptcha"]["enabled"] == "yes"
? "checked"
: "" ?>>
<label class="form-check-label fw-bold" for="security-recaptcha-enabled">
Enable reCAPTCHA v3
</label>
</div>
<div class="recaptcha-settings" style="<?= isset($checker['security']['recaptcha']['enabled']) && $checker['security']['recaptcha']['enabled'] == 'yes' ? '' : 'display:none;' ?>">
<div class="recaptcha-settings" style="<?= isset(
$checker["security"]["recaptcha"]["enabled"],
) && $checker["security"]["recaptcha"]["enabled"] == "yes"
? ""
: "display:none;" ?>">
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Site Key</label>
<input type="text" name="checker[security][recaptcha][site_key]" value="<?= $checker['security']['recaptcha']['site_key'] ?? '' ?>" class="form-control" placeholder="6Lc...">
<small class="text-muted">Public key for frontend</small>
<label class="form-label">Site Key <span class="text-danger">*</span></label>
<input type="text" name="checker[security][recaptcha][site_key]" value="<?= esc_attr($checker["security"]["recaptcha"]["site_key"] ?? "") ?>" class="form-control" placeholder="6Lc...">
<small class="text-muted">Public key for frontend - shown after creating reCAPTCHA site</small>
</div>
<div class="col-md-6">
<label class="form-label">Secret Key</label>
<input type="text" name="checker[security][recaptcha][secret_key]" value="<?= $checker['security']['recaptcha']['secret_key'] ?? '' ?>" class="form-control" placeholder="6Lc...">
<small class="text-muted">Private key for backend verification</small>
<label class="form-label">Secret Key (Server) <span class="text-danger">*</span></label>
<input type="text" name="checker[security][recaptcha][secret_key]" value="<?= esc_attr($checker["security"]["recaptcha"]["secret_key"] ?? "") ?>" class="form-control" placeholder="Secret key from Google admin console">
<small class="text-muted">Server-side secret from Google reCAPTCHA admin console (required for verification)</small>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Minimum Score</label>
<input type="number" name="checker[security][recaptcha][min_score]" value="<?= $checker['security']['recaptcha']['min_score'] ?? 0.5 ?>" class="form-control" min="0" max="1" step="0.1">
<small class="text-muted">0.0 (bot) to 1.0 (human). Recommended: 0.5</small>
<input type="number" name="checker[security][recaptcha][min_score]" value="<?= $checker[
"security"
]["recaptcha"]["min_score"] ??
0.5 ?>" class="form-control" min="0" max="1" step="0.1" <?= isset(
$checker["security"]["recaptcha"]["enabled"],
) && $checker["security"]["recaptcha"]["enabled"] == "yes"
? "required"
: "" ?>>
<small class="text-muted">0.0 (likely bot) to 1.0 (likely human). Recommended: 0.5</small>
</div>
<div class="col-md-6">
<label class="form-label">Action Name</label>
<input type="text" name="checker[security][recaptcha][action]" value="<?= $checker[
"security"
]["recaptcha"]["action"] ??
"checker_validate" ?>" class="form-control" <?= isset(
$checker["security"]["recaptcha"]["enabled"],
) && $checker["security"]["recaptcha"]["enabled"] == "yes"
? "required"
: "" ?>>
<small class="text-muted">Action name for reCAPTCHA tracking (letters only)</small>
</div>
</div>
<div class="row mb-3">
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="yes" id="security-recaptcha-hide-badge" name="checker[security][recaptcha][hide_badge]" <?= isset(
$checker["security"]["recaptcha"][
"hide_badge"
],
) &&
$checker["security"]["recaptcha"][
"hide_badge"
] == "yes"
? "checked"
: "" ?>>
<label class="form-check-label" for="security-recaptcha-hide-badge">
Hide reCAPTCHA Badge
</label>
</div>
<small class="text-muted">Hides the "protected by reCAPTCHA" badge. You must add attribution elsewhere on the page.</small>
</div>
</div>
<div class="row mb-3">
<div class="col-12">
<label class="form-label">Custom Error Message</label>
<input type="text" name="checker[security][recaptcha][error_message]" value="<?= esc_attr($checker["security"]["recaptcha"]["error_message"] ?? "") ?>" class="form-control" placeholder="<?= esc_attr__('Leave empty for default message', 'sheet-data-checker-pro') ?>">
<small class="text-muted"><?= esc_html__('Custom message shown when reCAPTCHA verification fails (leave empty for default)', 'sheet-data-checker-pro') ?></small>
</div>
</div>
</div>
</td>
</tr>
<tr class="has-link" style="display: none;">
<th>Cloudflare Turnstile</th>
<td>
<p class="text-muted small mb-3">Privacy-friendly CAPTCHA alternative. <a href="https://dash.cloudflare.com/?to=/:account/turnstile" target="_blank">Get keys here</a></p>
<div class="alert alert-info small mb-3">
<strong>How to get keys:</strong><br>
1. Go to <a href="https://dash.cloudflare.com/?to=/:account/turnstile" target="_blank" rel="noopener">Cloudflare Turnstile Dashboard</a><br>
2. Click "Add Widget" and enter your site name<br>
3. Add your domain (e.g., dwindi.com)<br>
4. Choose Widget Mode: <strong>Managed</strong> (recommended) or Non-interactive<br>
5. Copy the <strong>Site Key</strong> and <strong>Secret Key</strong> shown after creation
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="yes" id="security-turnstile-enabled" name="checker[security][turnstile][enabled]" <?= isset($checker['security']['turnstile']['enabled']) && $checker['security']['turnstile']['enabled'] == 'yes' ? 'checked' : '' ?>>
<input class="form-check-input" type="checkbox" value="yes" id="security-turnstile-enabled" name="checker[security][turnstile][enabled]" <?= isset(
$checker["security"]["turnstile"]["enabled"],
) && $checker["security"]["turnstile"]["enabled"] == "yes"
? "checked"
: "" ?>>
<label class="form-check-label fw-bold" for="security-turnstile-enabled">
Enable Cloudflare Turnstile
</label>
</div>
<div class="turnstile-settings" style="<?= isset($checker['security']['turnstile']['enabled']) && $checker['security']['turnstile']['enabled'] == 'yes' ? '' : 'display:none;' ?>">
<div class="turnstile-settings" style="<?= isset(
$checker["security"]["turnstile"]["enabled"],
) && $checker["security"]["turnstile"]["enabled"] == "yes"
? ""
: "display:none;" ?>">
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Site Key</label>
<input type="text" name="checker[security][turnstile][site_key]" value="<?= $checker['security']['turnstile']['site_key'] ?? '' ?>" class="form-control" placeholder="0x4AAA...">
<small class="text-muted">Public key for frontend</small>
<label class="form-label">Site Key <span class="text-danger">*</span></label>
<input type="text" name="checker[security][turnstile][site_key]" value="<?= esc_attr($checker["security"]["turnstile"]["site_key"] ?? "") ?>" class="form-control" placeholder="0x4AAA...">
<small class="text-muted">Public key for frontend (starts with 0x4AAA...)</small>
</div>
<div class="col-md-6">
<label class="form-label">Secret Key</label>
<input type="text" name="checker[security][turnstile][secret_key]" value="<?= $checker['security']['turnstile']['secret_key'] ?? '' ?>" class="form-control" placeholder="0x4AAA...">
<small class="text-muted">Private key for backend verification</small>
<label class="form-label">Secret Key <span class="text-danger">*</span></label>
<input type="text" name="checker[security][turnstile][secret_key]" value="<?= esc_attr($checker["security"]["turnstile"]["secret_key"] ?? "") ?>" class="form-control" placeholder="0x4AAA...">
<small class="text-muted">Private key for backend verification (starts with 0x4AAA...)</small>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Theme</label>
<select name="checker[security][turnstile][theme]" class="form-select">
<option value="light" <?= isset($checker['security']['turnstile']['theme']) && $checker['security']['turnstile']['theme'] == 'light' ? 'selected' : '' ?>>Light</option>
<option value="dark" <?= isset($checker['security']['turnstile']['theme']) && $checker['security']['turnstile']['theme'] == 'dark' ? 'selected' : '' ?>>Dark</option>
<option value="auto" <?= isset($checker['security']['turnstile']['theme']) && $checker['security']['turnstile']['theme'] == 'auto' ? 'selected' : '' ?>>Auto</option>
<option value="light" <?= isset(
$checker["security"]["turnstile"]["theme"],
) &&
$checker["security"]["turnstile"]["theme"] ==
"light"
? "selected"
: "" ?>>Light</option>
<option value="dark" <?= isset(
$checker["security"]["turnstile"]["theme"],
) &&
$checker["security"]["turnstile"]["theme"] ==
"dark"
? "selected"
: "" ?>>Dark</option>
<option value="auto" <?= isset(
$checker["security"]["turnstile"]["theme"],
) &&
$checker["security"]["turnstile"]["theme"] ==
"auto"
? "selected"
: "" ?>>Auto</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Size</label>
<select name="checker[security][turnstile][size]" class="form-select">
<option value="normal" <?= isset(
$checker["security"]["turnstile"]["size"],
) &&
$checker["security"]["turnstile"]["size"] ==
"normal"
? "selected"
: "" ?>>Normal</option>
<option value="compact" <?= isset(
$checker["security"]["turnstile"]["size"],
) &&
$checker["security"]["turnstile"]["size"] ==
"compact"
? "selected"
: "" ?>>Compact</option>
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-12">
<label class="form-label">Custom Error Message</label>
<input type="text" name="checker[security][turnstile][error_message]" value="<?= esc_attr($checker["security"]["turnstile"]["error_message"] ?? "") ?>" class="form-control" placeholder="<?= esc_attr__('Leave empty for default message', 'sheet-data-checker-pro') ?>">
<small class="text-muted"><?= esc_html__('Custom message shown when Turnstile verification fails (leave empty for default)', 'sheet-data-checker-pro') ?></small>
</div>
</div>
</div>
</td>
</tr>
<tr class="has-link" style="display: none;">
<th>Honeypot Protection</th>
<td>
<p class="text-muted small mb-3"><?= esc_html__('Invisible spam protection that catches automated bots without affecting real users', 'sheet-data-checker-pro') ?></p>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="yes" id="security-honeypot-enabled" name="checker[security][honeypot][enabled]" <?= isset($checker["security"]["honeypot"]["enabled"]) && $checker["security"]["honeypot"]["enabled"] == "yes" ? "checked" : "" ?>>
<label class="form-check-label fw-bold" for="security-honeypot-enabled">
<?= esc_html__('Enable Honeypot Field', 'sheet-data-checker-pro') ?>
</label>
</div>
<div class="honeypot-settings" style="<?= isset($checker["security"]["honeypot"]["enabled"]) && $checker["security"]["honeypot"]["enabled"] == "yes" ? "" : "display:none;" ?>">
<div class="row mb-3">
<div class="col-12">
<label class="form-label"><?= esc_html__('Custom Error Message', 'sheet-data-checker-pro') ?></label>
<input type="text" name="checker[security][honeypot][error_message]" value="<?= esc_attr($checker["security"]["honeypot"]["error_message"] ?? "") ?>" class="form-control" placeholder="<?= esc_attr__('Leave empty for default message', 'sheet-data-checker-pro') ?>">
<small class="text-muted"><?= esc_html__('Message shown when honeypot is triggered (leave empty for default)', 'sheet-data-checker-pro') ?></small>
</div>
</div>
<div class="alert alert-info small">
<i class="bi bi-info-circle"></i> <?= esc_html__('Honeypot adds an invisible field that bots will fill out, allowing easy detection without user interaction.', 'sheet-data-checker-pro') ?>
</div>
</div>
</td>
</tr>
<tr class="has-link" style="display: none;">
<th>IP Detection Method</th>
<td>
<p class="text-muted small mb-3">Configure how to detect visitor IP addresses</p>
<div class="row mb-3">
<div class="col-12">
<label class="form-label">IP Detection Priority</label>
<div class="form-check">
<input class="form-check-input" type="radio" id="ip-auto" name="checker[security][ip_detection]" value="auto" <?= !isset(
$checker["security"]["ip_detection"],
) || $checker["security"]["ip_detection"] == "auto"
? "checked"
: "" ?>>
<label class="form-check-label" for="ip-auto">
Automatic (Recommended)
</label>
<small class="d-block text-muted">Automatically detect IP through Cloudflare, proxies, and standard headers</small>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" id="ip-remote-addr" name="checker[security][ip_detection]" value="remote_addr" <?= isset(
$checker["security"]["ip_detection"],
) &&
$checker["security"]["ip_detection"] ==
"remote_addr"
? "checked"
: "" ?>>
<label class="form-check-label" for="ip-remote-addr">
REMOTE_ADDR Only
</label>
<small class="d-block text-muted">Only use REMOTE_ADDR (less accurate but more predictable)</small>
</div>
</div>
</div>
</td>
</tr>
<tr class="has-link" style="display: none;">
<th>Nonce Verification</th>
<td>
<p class="text-muted small mb-3">WordPress security token to prevent CSRF attacks</p>
<div class="row mb-3">
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="yes" id="security-nonce-enabled" name="checker[security][nonce_enabled]" value="yes" checked disabled>
<label class="form-check-label" for="security-nonce-enabled">
Enable Nonce Verification (Always Active)
</label>
</div>
<small class="text-muted">
<i class="fas fa-info-circle"></i>
Nonce verification is always enabled for security. This protects against Cross-Site Request Forgery (CSRF) attacks.
</small>
</div>
</div>
</td>
</tr>
<tr class="has-link" style="display: none;">
<th>Security Status</th>
<td>
<div id="security-status" class="alert alert-warning mb-3">
<i class="fas fa-exclamation-triangle"></i>
<strong>Security Check:</strong> Please configure at least one protection method (Rate Limiting, reCAPTCHA, or Turnstile).
</div>
<button type="button" class="btn btn-sm btn-outline-primary" id="test-security-btn">
<i class="fas fa-shield-alt"></i> Test Security Settings
</button>
<div id="security-test-results" style="display:none;" class="mt-3"></div>
</td>
</tr>
<tr class="has-link" style="display: none;">
<th colspan="2">
<div class="alert alert-info mb-0">
<strong>Note:</strong> Only enable ONE CAPTCHA solution at a time. reCAPTCHA and Turnstile cannot be used together.
<strong>Security Recommendations:</strong>
<ul class="mb-0 mt-2">
<li>Enable at least one protection method (Rate Limiting, reCAPTCHA, or Turnstile)</li>
<li>Only enable ONE CAPTCHA solution at a time (reCAPTCHA or Turnstile)</li>
<li>For high-traffic sites, use Rate Limiting with reCAPTCHA v3</li>
<li>Regularly review rate limiting logs for suspicious activity</li>
</ul>
</div>
</th>
</tr>
@@ -133,33 +420,178 @@ jQuery(document).ready(function($){
$('.rate-limit-settings').slideUp();
}
});
// Toggle IP whitelist
$('#security-rate-limit-whitelist').on('change', function(){
if($(this).is(':checked')){
$('#whitelist-ips').slideDown();
}else{
$('#whitelist-ips').slideUp();
}
});
// Toggle reCAPTCHA settings
$('#security-recaptcha-enabled').on('change', function(){
if($(this).is(':checked')){
$('.recaptcha-settings').slideDown();
// Disable Turnstile if reCAPTCHA is enabled
if($('#security-turnstile-enabled').is(':checked')){
$('#security-turnstile-enabled').prop('checked', false).trigger('change');
alert('reCAPTCHA enabled. Turnstile has been disabled.');
if(confirm('reCAPTCHA will be enabled. Do you want to disable Turnstile?')){
$('#security-turnstile-enabled').prop('checked', false).trigger('change');
} else {
$(this).prop('checked', false);
alert('Only one CAPTCHA solution can be active at a time.');
return false;
}
}
}else{
$('.recaptcha-settings').slideUp();
}
updateSecurityStatus();
});
// Toggle Turnstile settings
$('#security-turnstile-enabled').on('change', function(){
if($(this).is(':checked')){
$('.turnstile-settings').slideDown();
// Disable reCAPTCHA if Turnstile is enabled
if($('#security-recaptcha-enabled').is(':checked')){
$('#security-recaptcha-enabled').prop('checked', false).trigger('change');
alert('Turnstile enabled. reCAPTCHA has been disabled.');
if(confirm('Turnstile will be enabled. Do you want to disable reCAPTCHA?')){
$('#security-recaptcha-enabled').prop('checked', false).trigger('change');
} else {
$(this).prop('checked', false);
alert('Only one CAPTCHA solution can be active at a time.');
return false;
}
}
}else{
$('.turnstile-settings').slideUp();
}
updateSecurityStatus();
});
// Honeypot toggle
$('#security-honeypot-enabled').on('change', function(){
if($(this).is(':checked')){
$('.honeypot-settings').slideDown();
}else{
$('.honeypot-settings').slideUp();
}
updateSecurityStatus();
});
// Update security status when any setting changes
$('input[type="checkbox"]').on('change', function(){
updateSecurityStatus();
});
function updateSecurityStatus() {
var rateLimitEnabled = $('#security-rate-limit-enabled').is(':checked');
var recaptchaEnabled = $('#security-recaptcha-enabled').is(':checked');
var turnstileEnabled = $('#security-turnstile-enabled').is(':checked');
var honeypotEnabled = $('#security-honeypot-enabled').is(':checked');
var statusEl = $('#security-status');
var protectionCount = 0;
var protections = [];
if(rateLimitEnabled) { protectionCount++; protections.push('Rate Limiting'); }
if(recaptchaEnabled) { protectionCount++; protections.push('reCAPTCHA'); }
if(turnstileEnabled) { protectionCount++; protections.push('Turnstile'); }
if(honeypotEnabled) { protectionCount++; protections.push('Honeypot'); }
if(protectionCount > 0) {
statusEl.removeClass('alert-warning alert-danger').addClass('alert-success');
statusEl.html('<i class="fas fa-check-circle"></i> <strong>Security Status:</strong> ' + protectionCount + ' protection(s) active: ' + protections.join(', '));
} else {
statusEl.removeClass('alert-success alert-danger').addClass('alert-warning');
statusEl.html('<i class="fas fa-exclamation-triangle"></i> <strong>Security Status:</strong> No protection enabled. Please configure at least one protection method.');
}
// Check for CAPTCHA conflict
if(recaptchaEnabled && turnstileEnabled) {
statusEl.removeClass('alert-success alert-warning').addClass('alert-danger');
statusEl.html('<i class="fas fa-exclamation-circle"></i> <strong>Security Warning:</strong> Both reCAPTCHA and Turnstile are enabled. Please disable one of them.');
}
}
// Test security settings
$('#test-security-btn').on('click', function(){
var btn = $(this);
var resultsEl = $('#security-test-results');
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Testing...');
resultsEl.removeClass('alert-success alert-danger alert-warning').addClass('alert-info')
.html('<i class="fas fa-info-circle"></i> Running security tests...').show();
// Simulate security test
setTimeout(function(){
var issues = [];
// Check rate limiting
if(!$('#security-rate-limit-enabled').is(':checked')) {
issues.push('Rate limiting is not enabled');
}
// Check CAPTCHA
if(!$('#security-recaptcha-enabled').is(':checked') && !$('#security-turnstile-enabled').is(':checked')) {
issues.push('No CAPTCHA solution is enabled');
}
// Check for CAPTCHA conflict
if($('#security-recaptcha-enabled').is(':checked') && $('#security-turnstile-enabled').is(':checked')) {
issues.push('Both reCAPTCHA and Turnstile are enabled (conflict)');
}
// Check reCAPTCHA keys
if($('#security-recaptcha-enabled').is(':checked')) {
var siteKey = $('input[name="checker[security][recaptcha][site_key]"]').val();
var secretKey = $('input[name="checker[security][recaptcha][secret_key]"]').val();
if(!siteKey || !siteKey.startsWith('6Lc')) {
issues.push('reCAPTCHA site key is missing or invalid');
}
if(!secretKey || !secretKey.startsWith('6Lc')) {
issues.push('reCAPTCHA secret key is missing or invalid');
}
}
// Check Turnstile keys
if($('#security-turnstile-enabled').is(':checked')) {
var siteKey = $('input[name="checker[security][turnstile][site_key]"]').val();
var secretKey = $('input[name="checker[security][turnstile][secret_key]"]').val();
if(!siteKey || !siteKey.startsWith('0x4AAA')) {
issues.push('Turnstile site key is missing or invalid');
}
if(!secretKey || !secretKey.startsWith('0x4AAA')) {
issues.push('Turnstile secret key is missing or invalid');
}
}
// Display results
btn.prop('disabled', false).html('<i class="fas fa-shield-alt"></i> Test Security Settings');
if(issues.length === 0) {
resultsEl.removeClass('alert-info alert-danger alert-warning').addClass('alert-success')
.html('<i class="fas fa-check-circle"></i> <strong>All security checks passed!</strong> Your configuration is secure.');
} else {
var issuesHtml = '<ul class="mb-0">';
issues.forEach(function(issue) {
issuesHtml += '<li>' + issue + '</li>';
});
issuesHtml += '</ul>';
resultsEl.removeClass('alert-info alert-success alert-warning').addClass('alert-danger')
.html('<i class="fas fa-exclamation-circle"></i> <strong>Security issues found:</strong> ' + issuesHtml);
}
}, 1500);
});
// Initial security status update
updateSecurityStatus();
});
</script>

View File

@@ -8,11 +8,61 @@
</ul>
</div>
<div class="col-10">
<?php require_once SHEET_CHECKER_PRO_PATH . 'templates/editor/setting-table-card.php'; ?>
<?php require_once SHEET_CHECKER_PRO_PATH . 'templates/editor/setting-table-form.php'; ?>
<?php require_once SHEET_CHECKER_PRO_PATH . 'templates/editor/setting-table-result.php'; ?>
<?php require_once SHEET_CHECKER_PRO_PATH . 'templates/editor/setting-table-security.php'; ?>
<?php require_once SHEET_CHECKER_PRO_PATH . 'templates/editor/js-template-repeater-card.php'; ?>
<?php require_once SHEET_CHECKER_PRO_PATH . 'templates/editor/js-template-setting-output.php'; ?>
</div>
</div>
<?php require_once SHEET_CHECKER_PRO_PATH .
"templates/editor/setting-table-card.php"; ?>
<?php require_once SHEET_CHECKER_PRO_PATH .
"templates/editor/setting-table-form.php"; ?>
<?php require_once SHEET_CHECKER_PRO_PATH .
"templates/editor/setting-table-result.php"; ?>
<?php require_once SHEET_CHECKER_PRO_PATH .
"templates/editor/setting-table-security.php"; ?>
<?php require_once SHEET_CHECKER_PRO_PATH .
"templates/editor/js-template-repeater-card.php"; ?>
<?php require_once SHEET_CHECKER_PRO_PATH .
"templates/editor/js-template-setting-output.php"; ?>
<!-- Templates for preview functionality -->
<script id="vertical-table-template" type="text/x-handlebars-template">
<div class="dw-checker-results-container">
{{#each results}}
<div class="result-page" data-page="{{@index}}">
<table class="dw-checker-result-table" {{{getStyle ../resultDivider ../resultDividerWidth}}}>
<tbody>
{{#each this}}
{{#unless (getColumnSetting @key 'hide')}}
<tr>
<th {{{getStyleHeader ../resultDivider ../resultDividerWidth ../headerColor}}}}>
<span class="dw-checker-result-header">{{@key}}</span>
</th>
<td {{{getStyleValue ../resultDivider ../resultDividerWidth ../valueColor}}}>
{{#if (eq (getColumnSetting @key 'type') 'link_button')}}
<span class="dw-checker-result-value">{{getColumnSetting @key 'prefix' this}} <a href="{{getValueWithPrefix @key}}" class="btn btn-primary">{{getColumnSetting @key 'button_text'}}</a></span>
{{else if (eq (getColorSetting @key 'type') 'whatsapp_button')}}
<span class="dw-checker-result-value">{{getColumnSetting @key 'prefix' this}} <a href="https://wa.me/{{getValueWithPrefix @key}}" class="btn btn-success">{{getColumnSetting @key 'button_text'}}</a></span>
{{else}}
<span class="dw-checker-result-value">{{getColumnSetting @key 'prefix' this}} {{formatValue this}}</span>
{{/if}}
</td>
</tr>
{{/unless}}
{{/each}}
</tbody>
</table>
</div>
{{/each}}
</div>
</script>
<script id="div-template" type="text/x-handlebars-template">
<div class="dw-checker-results-container">
{{#each results}}
<div class="result-page" data-page="{{@index}}">
<div class="dw-checker-result-container" data-pagination="{{@index}}">
{{#each this}}
<div class="dw-checker-result-div">
<div class="result-header">
<span class="dw-checker-result-header">{{@key}}</span>
</div>
<div class="result-value">
<span class="dw-checker-result-value">{{#ifCond (eq (getColumnSetting @key 'type') 'text')}}{{formatValue this}}{{else}}{{#ifCond (eq (getColumnSetting @key 'type') 'link_button')}}{{getColumnSetting @key 'prefix' this}} <a href="{{this}}" class="btn btn-primary">{{getColumnSetting @key 'button_text'}}</a>{{else if (eq (getColumnSetting @key 'type') 'whatsapp_button')}}{{getColumnSetting @key 'prefix' this}} <a href="https://wa.me/{{this}}" class="btn btn-success">{{getColumnSetting @key 'button_text'}}
</div>