Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions admin/js/vendor/chart.umd.min.js

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions admin/partials/settings-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,17 @@
</select>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e('Challenge Page Credit', 'webdecoy'); ?></th>
<td>
<label>
<input type="checkbox" name="webdecoy_options[challenge_show_credit]" value="1"
<?php checked($options['challenge_show_credit'] ?? false); ?> />
<?php esc_html_e('Show a "Protected by WebDecoy" link on the challenge page', 'webdecoy'); ?>
</label>
<p class="description"><?php esc_html_e('Optional attribution shown to visitors who see the challenge page. Off by default.', 'webdecoy'); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="webdecoy_block_duration"><?php esc_html_e('Block Duration', 'webdecoy'); ?></label>
Expand Down
40 changes: 23 additions & 17 deletions admin/partials/statistics-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

// 30-day detection trend
$thirty_days_ago = gmdate('Y-m-d H:i:s', strtotime('-30 days'));
$daily_counts = $wpdb->get_results($wpdb->prepare(

Check warning on line 25 in admin/partials/statistics-page.php

View workflow job for this annotation

GitHub Actions / PHP Coding Standards

Direct database call without caching detected. Consider using wp_cache_get() / wp_cache_set() or wp_cache_delete().

Check warning on line 25 in admin/partials/statistics-page.php

View workflow job for this annotation

GitHub Actions / PHP Coding Standards

Use of a direct database call is discouraged.
"SELECT DATE(created_at) as date, COUNT(*) as count FROM {$detections_table} WHERE created_at > %s GROUP BY DATE(created_at) ORDER BY date ASC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix, not user input
$thirty_days_ago
), ARRAY_A);
Expand All @@ -40,13 +40,13 @@
}

// Threat level distribution
$threat_distribution = $wpdb->get_results($wpdb->prepare(

Check warning on line 43 in admin/partials/statistics-page.php

View workflow job for this annotation

GitHub Actions / PHP Coding Standards

Direct database call without caching detected. Consider using wp_cache_get() / wp_cache_set() or wp_cache_delete().

Check warning on line 43 in admin/partials/statistics-page.php

View workflow job for this annotation

GitHub Actions / PHP Coding Standards

Use of a direct database call is discouraged.
"SELECT threat_level, COUNT(*) as count FROM {$detections_table} WHERE created_at > %s GROUP BY threat_level ORDER BY count DESC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix, not user input
$thirty_days_ago
), ARRAY_A);

// Top flagged signals (parse from JSON flags)
$recent_flags = $wpdb->get_results($wpdb->prepare(

Check warning on line 49 in admin/partials/statistics-page.php

View workflow job for this annotation

GitHub Actions / PHP Coding Standards

Use of a direct database call is discouraged.
"SELECT flags FROM {$detections_table} WHERE created_at > %s AND flags IS NOT NULL AND flags != '' LIMIT 500", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix, not user input
$thirty_days_ago
), ARRAY_A);
Expand Down Expand Up @@ -244,20 +244,26 @@
<?php endif; ?>
</div>

<!-- Chart Data -->
<script>
window.webdecoyChartData = {
trend: {
labels: <?php echo wp_json_encode(array_keys($daily_data)); ?>,
data: <?php echo wp_json_encode(array_values($daily_data)); ?>
},
threats: {
labels: <?php echo wp_json_encode(array_column($threat_distribution, 'threat_level')); ?>,
data: <?php echo wp_json_encode(array_map('intval', array_column($threat_distribution, 'count'))); ?>
},
signals: {
labels: <?php echo wp_json_encode(array_keys($top_signals)); ?>,
data: <?php echo wp_json_encode(array_values($top_signals)); ?>
}
};
</script>
<?php
// Attach the chart data to the footer-enqueued charts script. The
// 'webdecoy-charts' handle is enqueued in admin_scripts() for this page hook
// with in_footer=true, so attaching inline data here (during page render,
// before admin footer scripts print) guarantees it lands before the script.
wp_add_inline_script(
'webdecoy-charts',
'window.webdecoyChartData = ' . wp_json_encode([
'trend' => [
'labels' => array_keys($daily_data),
'data' => array_values($daily_data),
],
'threats' => [
'labels' => array_column($threat_distribution, 'threat_level'),
'data' => array_map('intval', array_column($threat_distribution, 'count')),
],
'signals' => [
'labels' => array_keys($top_signals),
'data' => array_values($top_signals),
],
]) . ';',
'before'
);
7 changes: 7 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
*** WebDecoy Bot Detection Changelog ***

= 2.2.3 - 2026-07-21 =
* Changed: All CSS and JavaScript is now loaded via the WordPress dependency APIs — the challenge/block interstitials use wp_register_style/script + wp_print_styles/scripts with static asset files (public/css/, public/js/webdecoy-challenge.js), and the Statistics page attaches chart data via wp_add_inline_script. No more raw <style>/<script> tags.
* Changed: The "Protected by WebDecoy" credit on the challenge page is now an explicit admin opt-in (Settings → Blocking → Challenge Page Credit) and off by default, per WordPress.org guideline 10.
* Changed: Updated bundled Chart.js from v4.4.9 to v4.5.1 (latest stable).
* Changed: Removed load_plugin_textdomain() — unneeded since WordPress 4.6 for directory-hosted plugins (minimum is WP 6.1).
* Docs: readme now notes that good-bot documentation URLs in the bundled bot database are reference metadata only and are never requested.

= 2.2.2 - 2026-07-20 =
* Fixed: A PHP 7.4 fatal error in good-bot verification (str_ends_with is PHP 8.0+)
* Fixed: Timezone-safe date handling throughout (date() -> gmdate())
Expand Down
114 changes: 114 additions & 0 deletions public/css/webdecoy-block.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* WebDecoy Block Page styles
*
* Loaded on the standalone block interstitial via wp_register_style() +
* wp_print_styles() (the page renders before the theme, so the
* wp_enqueue_scripts hook never fires for it).
*
* @package WebDecoy
*/

* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.block-container {
background: #fff;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 100%;
padding: 40px;
text-align: center;
}
.block-icon {
width: 80px;
height: 80px;
background: #f44336;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 24px;
}
.block-icon svg {
width: 40px;
height: 40px;
fill: #fff;
}
h1 {
color: #333;
font-size: 28px;
margin-bottom: 16px;
font-weight: 600;
}
.block-message {
color: #666;
font-size: 16px;
line-height: 1.6;
margin-bottom: 24px;
}
.block-details {
background: #f5f5f5;
border-radius: 8px;
padding: 16px;
margin-bottom: 24px;
font-size: 14px;
color: #666;
}
.block-details dt {
font-weight: 600;
color: #333;
display: inline;
}
.block-details dd {
display: inline;
margin-left: 8px;
}
.block-details dl {
margin: 8px 0;
}
.contact-section {
border-top: 1px solid #eee;
padding-top: 24px;
margin-top: 24px;
}
.contact-section p {
color: #666;
font-size: 14px;
margin-bottom: 12px;
}
.contact-link {
display: inline-block;
background: #667eea;
color: #fff;
padding: 12px 24px;
border-radius: 6px;
text-decoration: none;
font-weight: 500;
transition: background 0.2s;
}
.contact-link:hover {
background: #5a6fd6;
}
.reference-id {
font-size: 12px;
color: #999;
margin-top: 24px;
}
.reference-id code {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
}
150 changes: 150 additions & 0 deletions public/css/webdecoy-challenge.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* WebDecoy Challenge Page styles
*
* Loaded on the standalone proof-of-work challenge interstitial via
* wp_register_style() + wp_print_styles() (the page renders before the
* theme, so the wp_enqueue_scripts hook never fires for it).
*
* @package WebDecoy
*/

* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
background: #1a1a2e;
color: #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
}
.challenge-card {
background: #16213e;
border: 1px solid #2a3a5c;
border-radius: 12px;
padding: 40px;
max-width: 440px;
width: 100%;
text-align: center;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
.challenge-icon {
width: 48px;
height: 48px;
margin: 0 auto 20px;
border: 3px solid #4a9eff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.challenge-icon svg {
width: 24px;
height: 24px;
fill: #4a9eff;
}
h1 {
font-size: 20px;
font-weight: 600;
color: #fff;
margin-bottom: 8px;
}
.subtitle {
font-size: 14px;
color: #8892a4;
margin-bottom: 24px;
}
.challenge-checkbox {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 16px 24px;
border: 2px solid #2a3a5c;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
background: #1a1a2e;
user-select: none;
margin-bottom: 16px;
}
.challenge-checkbox:hover {
border-color: #4a9eff;
background: #1e2a4a;
}
.challenge-checkbox.solving {
border-color: #ffa726;
cursor: wait;
}
.challenge-checkbox.solving:hover {
background: #1a1a2e;
}
.challenge-checkbox.success {
border-color: #66bb6a;
background: #1a2e1a;
cursor: default;
}
.challenge-checkbox.failed {
border-color: #ef5350;
}
.checkbox-box {
width: 24px;
height: 24px;
border: 2px solid #4a5568;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s;
}
.checkbox-box .check {
display: none;
color: #66bb6a;
font-size: 16px;
font-weight: bold;
}
.checkbox-box .spinner {
display: none;
width: 16px;
height: 16px;
border: 2px solid #ffa726;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.success .checkbox-box { border-color: #66bb6a; }
.success .checkbox-box .check { display: block; }
.solving .checkbox-box .spinner { display: block; }
.checkbox-label {
font-size: 15px;
color: #c0c8d8;
}
.solving .checkbox-label { color: #ffa726; }
.success .checkbox-label { color: #66bb6a; }
.failed .checkbox-label { color: #ef5350; }
.status-text {
font-size: 12px;
color: #6b7280;
margin-top: 8px;
min-height: 18px;
}
.powered-by {
margin-top: 20px;
font-size: 11px;
color: #4a5568;
}
.powered-by a { color: #4a9eff; text-decoration: none; }
.retry-btn {
display: none;
margin-top: 12px;
padding: 8px 20px;
background: #4a9eff;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.retry-btn:hover { background: #3a8eef; }
@keyframes spin { to { transform: rotate(360deg); } }
Loading
Loading