Compare commits

...
Author SHA1 Message Date
e6cba7ef6a v5.1.6 2026-08-23 07:08:42 +00:00
7dfff83511 v5.1.2 2026-08-07 10:44:58 +00:00
db98c4438c v5.1.1 2026-07-09 06:28:55 +00:00
22 changed files with 2775 additions and 1609 deletions

View file

@ -49,15 +49,15 @@ document.addEventListener(
var notifyOptions = [ var notifyOptions = [
{ {
checkboxSelector: 'input[name="wpvulnerability-config[notify][email]"]', checkboxSelector: 'input[type="checkbox"][name="wpvulnerability-config[notify][email]"]',
fieldId: 'wpvulnerability_emails', fieldId: 'wpvulnerability_emails',
}, },
{ {
checkboxSelector: 'input[name="wpvulnerability-config[notify][slack]"]', checkboxSelector: 'input[type="checkbox"][name="wpvulnerability-config[notify][slack]"]',
fieldId: 'wpvulnerability_slack_webhook', fieldId: 'wpvulnerability_slack_webhook',
}, },
{ {
checkboxSelector: 'input[name="wpvulnerability-config[notify][teams]"]', checkboxSelector: 'input[type="checkbox"][name="wpvulnerability-config[notify][teams]"]',
fieldId: 'wpvulnerability_teams_webhook', fieldId: 'wpvulnerability_teams_webhook',
}, },
]; ];

50
blueprint.json Normal file
View file

@ -0,0 +1,50 @@
{
"$schema": "https://playground.wordpress.net/blueprint-schema.json",
"landingPage": "/wp-admin/plugins.php",
"login": true,
"siteOptions": {
"blogname": "WPVulnerability Demo (old versions)"
},
"steps": [
{
"step": "installPlugin",
"pluginZipFile": {
"resource": "url",
"url": "https://downloads.wordpress.org/plugin/updraftplus.1.12.29.zip"
},
"options": {
"activate": false
}
},
{
"step": "installPlugin",
"pluginZipFile": {
"resource": "url",
"url": "https://downloads.wordpress.org/plugin/woocommerce.9.9.6.zip"
},
"options": {
"activate": false
}
},
{
"step": "installTheme",
"themeZipFile": {
"resource": "url",
"url": "https://downloads.wordpress.org/theme/bingle.1.0.4.zip"
},
"options": {
"activate": false
}
},
{
"step": "installPlugin",
"pluginZipFile": {
"resource": "url",
"url": "https://downloads.wordpress.org/plugin/wpvulnerability.zip"
},
"options": {
"activate": true
}
}
]
}

View file

@ -1,5 +1,196 @@
== Changelog == == Changelog ==
= [5.1.6] - 2026-08-22 =
**Fixed**
* Saving the "Delete all plugin data on uninstall" preference no longer triggers a fatal error (`add_settings_error()` was called before the WordPress admin API was loaded).
* Notification channels can now be disabled entirely: unchecking every channel and saving previously kept the old channels active, because unchecked checkboxes are never submitted.
* "Reset Plugin" and "Delete all logs" now purge both log stores (API request logs and shell execution audit logs). Previously the shell audit store was never cleaned, and both deletions silently did nothing because the log post types were not registered yet when the actions ran.
* The Debug tab API connectivity tests now use the real API routes: WordPress core collapses pre-release versions (e.g. 7.1-alpha) to their stable milestone, and plugins/themes are tested against their actual slug routes instead of nonexistent plural ones (HTTP 404).
* Component icons now carry explicit width/height attributes and a meaningful alt text. The same icons are embedded in notification emails and Site Health, where the plugin CSS is not loaded and unsized SVGs rendered at their intrinsic 800x800 size.
**Changed**
* `languages/wpvulnerability.pot` regenerated with the new strings.
**Compatibility**
* WordPress: 4.7 - 7.1
* PHP: 7.0 - 8.5
* WP-CLI: 2.3.0 - 2.12.0
**Tests**
* PHP Coding Standards: 3.13.6 (0 errors)
* WordPress Coding Standards: 3.4.1
* PHPStan: 2.2.9 (level 9, 0 errors)
* PHPUnit: 9.6.36 (54 tests)
= [5.1.5] - 2026-08-22 =
**Changed**
* The contributor list now leads with the ROBOTSTXT organization account, followed by Javier Casares; the remaining contributors are unchanged.
* Regenerated `languages/wpvulnerability.pot` so its source references match the current code. The string set is unchanged (450 strings); all shipped translations were verified against it and remain fully up to date.
**Compatibility**
* WordPress: 4.7 - 7.1
* PHP: 7.0 - 8.5
* WP-CLI: 2.3.0 - 2.12.0
**Tests**
* PHP Coding Standards: 3.13.6
* WordPress Coding Standards: 3.4.1
* PHPStan: 2.2.9 (level 9, 0 errors)
* PHPUnit: 9.6.36 (45 tests)
= [5.1.4] - 2026-08-22 =
**Highlights**
* The Site Health vulnerability tests work again on every WordPress version: they were silently disabled everywhere by an availability gate that could never pass. Secrets are now masked in the admin forms, and compatibility was verified against real PHP interpreters from 5.6 to 8.5.
**Fixed**
* The Site Health integration registered its tests behind `class_exists( 'WP_Site_Health' )`, but core loads that class lazily, after `init`, where the plugin loads its modules — so the guard always evaluated to false and none of the 13 vulnerability tests ever appeared in Site Health. The filter is now registered unconditionally; below WordPress 5.2 it simply never fires.
* "Clear all caches" in the debug tools now also deletes the `wpvulnerability-plugins-cache-data` timestamp option, so the plugins dataset is fully refreshed afterwards.
* The notification email "From:" address is now sanitized with `sanitize_email()` on all code paths.
* The plugins list table "Last updated" column decoded the whole plugins dataset for every table row; it is now decoded once per request.
* Removed a duplicated `json_decode()` call when reading the vulnerable-themes count.
* The `wp_date()` compatibility polyfill no longer fatals on WordPress 4.75.3: core calls `wp_maybe_decline_date()` unconditionally, but that function only exists since 5.4; the call is now availability-guarded.
* The core and software JSON getters return `'[]'` instead of an empty string when encoding fails, matching the plugins and themes getters.
**Security**
* Webhook URLs (Slack, Teams, Discord) and the Telegram bot token are masked in the admin settings forms (single-site and network), showing only their last 4 characters — previously they were rendered in full into the page HTML. The inputs are plain text fields, so masked values never trigger browser URL validation. Saving a form with unchanged masked values keeps the stored secret; clearing a field still removes it.
* Removed the dead `register_uninstall_hook()` registration: `uninstall.php` supersedes it and the registration added a stray autoloaded option row.
* Single-site settings registrations now explicitly set `show_in_rest => false`, matching the multisite configuration.
**Changed**
* Admin CSS/JS assets load only on the WPVulnerability settings pages, the dashboard and the plugins/themes list tables instead of on every wp-admin screen.
* The shell availability probe (`wpvulnerability_can_shell_exec()`) result is cached per request instead of spawning a process on every call.
**Compatibility**
* WordPress: 4.7 - 7.1
* PHP: 7.0 - 8.5
* WP-CLI: 2.3.0 - 2.12.0
* Verified against real PHP interpreters 5.6.40, 7.0.33, 7.1.33, 7.2.34, 7.3.33, 7.4.33, 8.0.30, 8.1.34, 8.2.33, 8.3.33, 8.4.24 and 8.5.9: every plugin file lints clean from 7.0 up and fails on 5.6, confirming the declared floor. PHPCompatibility gate 7.0-8.5 clean; wp-compat WordPress floor 4.7 clean.
**Tests**
* PHP Coding Standards: 3.13.6
* WordPress Coding Standards: 3.4.1
* PHPStan: 2.2.9 (level 9, 0 errors)
* PHPUnit: 9.6.36 (45 tests)
= [5.1.3] - 2026-08-22 =
**Added**
* `wpvulnerability-compat.php`: verbatim WordPress core function polyfills (`wp_doing_cron`, `get_main_site_id`, `wp_is_json_request`, `sanitize_locale_name`, `determine_locale`, `wp_timezone_string`, `wp_timezone`, `wp_date`, `get_post_datetime`, `get_post_timestamp`), each guarded by `function_exists()`. The plugin no longer fatals on WordPress versions that lack these functions.
**Security**
* The API connectivity test results (message, HTTP code, response time and the raw response preview) are now escaped client-side before being inserted into the page, in both single-site and network admin. Remote API responses could previously inject arbitrary HTML.
* The debug export and the Database Options Viewer now mask webhook URLs, Telegram bot tokens/chat IDs and notification email addresses, so shared debug files never contain usable secrets.
* The multisite "components to hide" save now routes through the strict sanitizer, so a crafted request can no longer re-enable analysis of components force-hidden with `WPVULNERABILITY_HIDE_*` constants.
* API responses are only cached when the HTTP status is 200 and the body is valid JSON. Error responses (4xx/5xx, HTML error pages) were previously cached for the full TTL and blocked retries.
* API request URLs are now built from whitelisted types and sanitized slugs/versions (`wpvulnerability_get()`, `wpvulnerability_get_vulnerabilities()`).
**Fixed**
* Cron scheduling ran `wp_get_schedules()` (and other plugins' `cron_schedules` callbacks, including their translation calls) before `init` on every request. Core and notification event scheduling moved to `init`, matching the WordPress 6.7+ translation-loading requirements.
* Deactivation now removes the `wpvulnerability-plugins-data` and `wpvulnerability-plugins-data-cache` options created at activation.
* The notification cron period is validated against `daily`/`weekly` before `wp_schedule_event()`, so a corrupted configuration value can no longer silently unschedule notifications.
* The admin menu vulnerability counters escape the badge title attribute, and the plugin row "Settings" link escapes its URL and label.
* Application Passwords REST authentication is gated on `function_exists( 'wp_is_application_passwords_available' )` so WordPress < 5.6 no longer fatals on unauthenticated REST requests; cookie-authenticated admin sessions keep working and the Application Passwords functionality itself is simply unavailable on old versions.
* The Site Health integration registers its tests only when `WP_Site_Health` exists (WordPress 5.2+); on older versions the plugin runs without the Site Health section.
**Changed**
* Minimum WordPress version lowered from 5.6 to 4.7. Whole functionalities degrade gracefully on old versions: Application Passwords REST authentication (WordPress 5.6+) and the Site Health integration (WordPress 5.2+) are only registered when available; everything else works through the polyfills.
**Compatibility**
* WordPress: 4.7 - 7.1
* PHP: 7.0 - 8.5
* WP-CLI: 2.3.0 - 2.12.0
**Tests**
* PHP Coding Standards: 3.13.6
* WordPress Coding Standards: 3.4.1
* PHPStan: 2.2.9 (level 9, 0 errors)
* PHPUnit: 9.6.36 (37 tests)
= [5.1.2] - 2026-08-07 =
**Fixed**
* Missing `load_plugin_textdomain()` call caused a "Translation loading for the `wpvulnerability` domain was triggered too early" `_doing_it_wrong()` notice on WordPress 6.7+. The textdomain is now explicitly loaded on the `init` hook.
**Added**
* WordPress Playground blueprint (`blueprint.json`) with pre-installed vulnerable plugins and a theme for quick testing.
**Changed**
* Security vulnerability reporting link updated to [ROBOTSTXT](https://www.robotstxt.es/contacto/).
* Contributor repository link updated to [git.robotstxt.es/ROBOTSTXT/wpvulnerability](https://git.robotstxt.es/ROBOTSTXT/wpvulnerability).
* The `readme.txt` Changelog section now shows only the latest 3 versions; the full history remains in `changelog.txt`.
**Compatibility**
* WordPress: 5.6 - 7.1
* PHP: 7.0 - 8.5
* WP-CLI: 2.3.0 - 2.12.0
**Tests**
* PHP Coding Standards: 3.13.5
* WordPress Coding Standards: 3.3.0
* PHPStan: 2.1.55 (level 9, 0 errors)
* PHPUnit: 9.6.34 (25 tests)
= [5.1.1] - 2026-07-09 =
**Fixed**
* Site Health: the memcached, Redis, and SQLite vulnerability tests always returned "Invalid software type" because those components were missing from the software list. They now run correctly.
* "Send test email" failed when no email recipients were configured: the forced-test path added the admin email but the send gate used a flag computed before the override. The test email now sends.
* "Run notification now" reported failure when only webhook channels (Slack/Teams/Discord/Telegram) were enabled and email was disabled. The notification result now reflects whether any channel delivered successfully.
* Weekly notification cron events were never auto-scheduled, because the `weekly` cron schedule was registered only at `init` while the on-load scheduling runs earlier. The weekly/daily schedule registration moved to the always-loaded scheduler so weekly notifications schedule correctly.
* Deactivation deleted the per-component analysis settings (`wpvulnerability-analyze`), so deactivating and reactivating reset which components were hidden. Analysis settings are now preserved on deactivation (only uninstall removes them).
* The debug "Cron Status" panel always showed the notification event as "not scheduled" because it checked the wrong hook name (`wpvulnerability_send_notification` instead of `wpvulnerability_notification`).
* The debug "last run" timestamp was always empty because it read a `wpvulnerability-logs` option that is never written; it now reads the most recent API log entry.
* Redis detection called `close()` twice on the success path (once in the `try` block, once in `finally`); the redundant close was removed.
* The single-site vulnerable-themes count was hardcoded to `0` instead of reading the stored option.
**Changed**
* Single-site email recipient sanitization now uses `is_email()` for strict validation, matching the multisite behaviour.
* Removed a redundant nonce field from the single-site "Reset Plugin" form (the form only submits the full-reset action).
* Removed dead code: the unused `wpvulnerability_sanitize_messages` callback and its setting registration, and an unused `$tools_action` variable in the multisite admin.
**Compatibility**
* WordPress: 5.6 - 7.1
* PHP: 7.0 - 8.5
* WP-CLI: 2.3.0 - 2.12.0
**Tests**
* PHP Coding Standards: 3.13.5
* WordPress Coding Standards: 3.3.0
* PHPStan: 2.1.55 (level 9, 0 errors)
* PHPUnit: 9.6.34 (25 tests)
= [5.1.0] - 2026-07-08 = = [5.1.0] - 2026-07-08 =
**Security** **Security**
@ -21,6 +212,19 @@
* Removed the unused `$plugin_status` parameter from `wpvulnerability_plugin_info_after()`; the PHPCS suite now passes with zero warnings. * Removed the unused `$plugin_status` parameter from `wpvulnerability_plugin_info_after()`; the PHPCS suite now passes with zero warnings.
* Multisite network dashboard: the "Site Health" footer link pointed to `wp-admin/network/site-health.php`, which does not exist (Site Health is a per-site screen). It now links to the main site's `wp-admin/site-health.php`. * Multisite network dashboard: the "Site Health" footer link pointed to `wp-admin/network/site-health.php`, which does not exist (Site Health is a per-site screen). It now links to the main site's `wp-admin/site-health.php`.
**Compatibility**
* WordPress: 5.6 - 7.1
* PHP: 7.0 - 8.5
* WP-CLI: 2.3.0 - 2.12.0
**Tests**
* PHP Coding Standards: 3.13.5
* WordPress Coding Standards: 3.3.0
* PHPStan: 2.1.55 (level 9, 0 errors)
* PHPUnit: 9.6.34 (18 tests)
= [5.0.1] - 2026-06-02 = = [5.0.1] - 2026-06-02 =
**Fixed** **Fixed**

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,11 @@
=== WPVulnerability === === WPVulnerability ===
Contributors: javiercasares, davidperez, lbonomo, alexclassroom Contributors: robotstxt, javiercasares, davidperez, lbonomo, alexclassroom
Tags: security, vulnerability, site-health Tags: security, vulnerability, site-health
Requires at least: 5.6 Requires at least: 4.7
Tested up to: 7.1 Tested up to: 7.1
Stable tag: 5.1.0 Stable tag: 5.1.6
Requires PHP: 7.0 Requires PHP: 7.0
Version: 5.1.0 Version: 5.1.6
License: GPL-3.0-or-later License: GPL-3.0-or-later
License URI: https://spdx.org/licenses/GPL-3.0-or-later.html License URI: https://spdx.org/licenses/GPL-3.0-or-later.html
@ -195,305 +195,100 @@ First of all, peace of mind. Investigate what the vulnerability is and, above al
== Compatibility == == Compatibility ==
* WordPress: 5.6 - 7.1 * WordPress: 4.7 - 7.1
* PHP: 7.0 - 8.5 * PHP: 7.0 - 8.5
* WP-CLI: 2.3.0 - 2.12.0 * WP-CLI: 2.3.0 - 2.12.0
== Changelog == == Changelog ==
= [5.1.0] - 2026-07-08 = = [5.1.6] - 2026-08-22 =
**Security**
* `wpvulnerability_validate_shell_command()` now uses exact `in_array()` match instead of `stripos()` substring matching for the shell-command allowlist (defense-in-depth).
* `wpvulnerability_detect_php()`, `wpvulnerability_detect_curl()`, and `wpvulnerability_detect_webserver()` now route through `wpvulnerability_safe_shell_exec()`, so every software-detection shell call is validated and recorded in the Shell Execution Audit Log. Previously these called `shell_exec()` directly, bypassing the wrapper and the audit log. As a side effect this also fixes nginx/angie version detection: `escapeshellcmd()` was escaping the `2>&1` redirect, so stderr (where nginx prints its version) was never captured.
**Fixed** **Fixed**
* Multisite uninstall fatal error: `Uncaught Error: Undefined constant "WPVULNERABILITY_PLUGIN_BASE"` when "Delete all plugin data on uninstall" was enabled. `uninstall.php` now defines the constant before loading `wpvulnerability-run.php`. * Saving the "Delete all plugin data on uninstall" preference no longer triggers a fatal error (`add_settings_error()` was called before the WordPress admin API was loaded).
* `WPVULNERABILITY_HIDE_*` constants now stop `shell_exec` detection for hidden components during scheduled scans and in the admin "Software Detection Methods" panel. Previously they only hid the results from the UI, so the audit log kept filling with "command not found" entries for components the administrator had explicitly deactivated. * Notification channels can now be disabled entirely: unchecking every channel and saving previously kept the old channels active, because unchecked checkboxes are never submitted.
* `wpvulnerability_detect_webserver()` no longer shell-probes a hidden web server via the sibling path: `WPVULNERABILITY_HIDE_NGINX` and `WPVULNERABILITY_HIDE_APACHE` now fully isolate the hidden server. * "Reset Plugin" and "Delete all logs" now purge both log stores (API request logs and shell execution audit logs). Previously the shell audit store was never cleaned, and both deletions silently did nothing because the log post types were not registered yet when the actions ran.
* Multisite cron requests on non-main subsites no longer load all plugin module files. No WPVulnerability cron events are scheduled on subsites, so the extra loading was wasted work. * The Debug tab API connectivity tests now use the real API routes: WordPress core collapses pre-release versions (e.g. 7.1-alpha) to their stable milestone, and plugins/themes are tested against their actual slug routes instead of nonexistent plural ones (HTTP 404).
* LiteSpeed / OpenLiteSpeed / Caddy shell detection in the WP_DEBUG diagnostic panel now works: the commands no longer use `2>/dev/null` (which the shell-command validator rejected), `caddy` was added to the allowlist, and `which` output is validated as a real path so "command not found" messages are not mistaken for a detection. * Component icons now carry explicit width/height attributes and a meaningful alt text. The same icons are embedded in notification emails and Site Health, where the plugin CSS is not loaded and unsized SVGs rendered at their intrinsic 800x800 size.
**Changed** **Changed**
* `Network: true` added to the plugin header to declare network-aware multisite behaviour. * `languages/wpvulnerability.pot` regenerated with the new strings.
* Removed the unused `$plugin_status` parameter from `wpvulnerability_plugin_info_after()`; the PHPCS suite now passes with zero warnings.
* Multisite network dashboard: the "Site Health" footer link pointed to `wp-admin/network/site-health.php`, which does not exist (Site Health is a per-site screen). It now links to the main site's `wp-admin/site-health.php`.
= [5.0.1] - 2026-06-02 =
**Fixed**
* `wp_cache_flush()` removed from the plugin/theme update hook and the plugin reset routine. The function was wiping the entire Object Cache (Redis, Memcached, APCu) on every update, causing CPU spikes on high-traffic sites. The targeted transient cleanup that already runs before it is sufficient.
= [5.0.0] - 2026-05-26 =
**Added**
* Vulnerability detail panels fully redesigned: Bootstrap-inspired score/severity badge, source attribution pills (hostname extracted from URL), and a structured "References:" row on the same line as the pills.
* CVSS priority order: CVSS 4 > CVSS 3 > CVSS 2 > legacy CVSS. The highest available score and severity are always shown.
* `ssvc.exploitation` field displayed: `poc` → "⚡ Public exploit" badge; `active` → included in the "Actively exploited" KEV label.
* `ssvc.automatable: yes` → "⚙ Automatable" badge alongside KEV/PoC.
* `ssvc.kev_date` → date appended to the "Actively exploited" label (e.g. "⚠ Actively exploited · 2024-03-15").
* `epss` field → "EPSS X.X%" badge on the same line as the CVSS score badge.
* CVE description (`source[].description`) now displayed for all vulnerability types: plugins, themes, core, and server software. Language prefix (e.g. `[en-US]`) stripped automatically.
* CWE name and description displayed in server software vulnerability details (was missing).
* Core vulnerabilities on `update-core.php` now rendered with the same badges, description, and pills as plugin/theme rows (replaces the old plain table).
* Theme details modal (single-site): vulnerability section injected after the Tags field using the `wp_prepare_themes_for_js` filter and a JS template patch. Shows the same version range + badge + description + references layout as the plugin rows.
* Version range display: leading `-` or `*` stripped when there is no lower bound (e.g. `- < 1.3.28` → `< 1.3.28`).
* Helper functions added to `wpvulnerability-process.php`: `wpvulnerability_source_css_slug()`, `wpvulnerability_render_source_pills()`, `wpvulnerability_render_score_badge()`, `wpvulnerability_clean_version_range()`, `wpvulnerability_get_source_description()`.
* Helper functions added to `wpvulnerability-themes.php`: `wpvulnerability_theme_modal_html()`, `wpvulnerability_filter_prepare_themes_for_js()`, `wpvulnerability_theme_modal_template_patch()`.
* KEV badge displayed in vulnerability details when the CVE is listed in the CISA Known Exploited Vulnerabilities (KEV) catalog (`impact.ssvc.kev` for plugins/themes/core; `impact.kev` for server software).
* Vulnerability severity now uses the full-word value from `cvss3.severity` (`"critical"`, `"high"`, `"medium"`, `"low"`) when available, falling back to the legacy single-character `cvss.severity` code.
* Publication date shown next to each source link in vulnerability details (`source[].date` field from the API).
* Server software lifecycle status (Supported / End of Life + EOL date) in vulnerability details and dashboard grid (`data.status`, `data.date_end` fields from the API).
* EOL badge in the dashboard software grid (PHP, Apache, nginx, MariaDB, MySQL, ImageMagick, curl, memcached, Redis, SQLite) when the detected version has reached end-of-life.
* REST API: all vulnerability endpoints now expose `uuid`, `kev` (boolean), `ssvc` block, and `severity` from `cvss3`. Source entries include a `date` field.
* About tab: new **Intelligence Sources** table showing per-source vulnerability counts (CVE, EUVD, JVN, Patchstack, WPScan, Wordfence) broken down by Core / Plugins / Themes.
**Security**
* `wpvulnerability-adminms.php`: config form handler now requires `manage_network_options` capability (via `current_user_can()`) in addition to the nonce check. Previously a user with a valid nonce could update plugin options without the required capability.
* WP-CLI: all vulnerability commands (`core`, `plugins`, `themes`, and all software components) and all config commands (`hide`, `email`, `cache`, `log-retention`, `period`) now require `manage_options` / `manage_network_options`. Commands abort with a clear error and `--user=<admin_login>` hint if the check fails.
* REST API permission check now passes strict mode (`true`) to `base64_decode()` when parsing the HTTP Basic Authorization header. A malformed or padding-stripped token is immediately rejected instead of decoding unpredictable bytes.
* Debug web server detection (LiteSpeed, OpenLiteSpeed, Caddy) now routes through the `wpvulnerability_safe_shell_exec()` wrapper, ensuring `WPVULNERABILITY_DISABLE_SHELL_EXEC`, security mode, command whitelist, and audit logging are respected even when `WP_DEBUG` is enabled.
* Page render callbacks (`wpvulnerability_create_admin_page`, `wpvulnerability_admin_dashboard_content`) now include explicit `current_user_can()` / `wp_die()` guards as defense-in-depth, per the WordPress Plugin Handbook.
**Fixed**
* Forced test email on a fresh install (no email address configured yet) no longer produces an `E_WARNING` on PHP 7.x or a `TypeError` on PHP 8.x.
* `determine_locale()` call is now guarded by `function_exists()` instead of `version_compare()`, which is more forward-compatible and correctly recognised by static analysis.
* `preg_replace()` return values in the HTML-to-plain-text converter are now handled safely when `null` is returned on error.
* `DOMDocument::$documentElement` null check added in the HTML-to-plain-text converter.
* JSON-encoded vulnerability count options are validated as strings before `json_decode()` throughout the codebase.
* `cron_schedules` callbacks now use `did_action('init')` guard before `__()`, eliminating the textdomain-too-early notice introduced in WordPress 6.7.
* WP-CLI vulnerability commands now display a clear success message when no vulnerabilities are found, instead of an empty table.
* WP-CLI `config period monthly` now returns a proper error. Valid periods: `daily`, `weekly`, `never`.
* `uninstall.php` created. Plugin data is preserved by default; a new "Delete all plugin data on uninstall" checkbox in the Tools tab allows opting in to full removal.
* Large vulnerability data options are now stored with `autoload=false`, preventing unnecessary loading of JSON blobs on every WordPress request.
**Changed**
* Minimum required PHP version raised from 5.6 to 7.0. The null coalescing operator (`??`) used throughout for improved type safety requires PHP 7.0. PHP 5.6 reached end-of-life in December 2018.
* Minimum required WordPress version raised from 4.7 to 5.6, enabling full use of Application Passwords, `wp_timezone()`, `wp_date()`, and `determine_locale()` without version guards.
* `wpvulnerability_get_cron_snapshot()` now uses only public WordPress Cron API functions (`wp_next_scheduled()`, `wp_get_schedule()`); `_get_cron_array()` (private WP API) removed.
**Developer**
* PHPStan level 9: 0 errors across all 18 PHP files; 1,092 pre-existing type errors resolved; baseline is now empty.
* Added dev tooling: `composer.json`, `phpstan.neon` (level 9), `phpcs.xml`, `phpunit.xml.dist`, `bin/deploy.sh`, `docs/`.
* Added 18 PHPUnit plugin header tests.
**Compatibility** **Compatibility**
* WordPress: 5.6 - 7.1 * WordPress: 4.7 - 7.1
* PHP: 7.0 - 8.5 * PHP: 7.0 - 8.5
* WP-CLI: 2.3.0 - 2.12.0 * WP-CLI: 2.3.0 - 2.12.0
**Tests** **Tests**
* PHP Coding Standards: 3.13.5 * PHP Coding Standards: 3.13.6 (0 errors)
* WordPress Coding Standards: 3.3.0 * WordPress Coding Standards: 3.4.1
* PHPStan: 2.1.55 (level 9, 0 errors) * PHPStan: 2.2.9 (level 9, 0 errors)
* PHPUnit: 9.6.34 (18 tests) * PHPUnit: 9.6.36 (54 tests)
* Plugin Check (PCP): 1.8.0
= [4.3.2] - 2026-05-20 = = [5.1.5] - 2026-08-22 =
**Performance**
* Notification scheduling no longer rewrites the `cron` row in `wp_options` on every WordPress request. The boot-time call now verifies the current schedule with `wp_get_schedule()` and only clears or reschedules the `wpvulnerability_notification` event when the desired period actually differs from the currently scheduled one. This eliminates 1-2 unnecessary `UPDATE wp_options` queries per page load, removing lock contention under concurrent load and the corresponding entries from the MariaDB/MySQL slow query log.
**Changed** **Changed**
* `wpvulnerability_schedule_notification_event()` gained an optional `$force` parameter (default `true`, backwards compatible). The boot-time invocation passes `false` to opt into the idempotent path; all other callers (admin save handlers, WP-CLI config command, update flow, repair helpers) keep the default and continue forcing a reschedule so that changes to hour/minute/day still update the next-run timestamp. * The contributor list now leads with the ROBOTSTXT organization account, followed by Javier Casares; the remaining contributors are unchanged.
* Regenerated `languages/wpvulnerability.pot` so its source references match the current code. The string set is unchanged (450 strings); all shipped translations were verified against it and remain fully up to date.
**Compatibility** **Compatibility**
* WordPress: 4.7 - 6.9 * WordPress: 4.7 - 7.1
* PHP: 5.6 - 8.5 * PHP: 7.0 - 8.5
* WP-CLI: 2.3.0 - 2.11.0 * WP-CLI: 2.3.0 - 2.12.0
**Tests** **Tests**
* PHP Coding Standards: 3.13.5 * PHP Coding Standards: 3.13.6
* WordPress Coding Standards: 3.3.0 * WordPress Coding Standards: 3.4.1
* Plugin Check (PCP): 1.8.0 * PHPStan: 2.2.9 (level 9, 0 errors)
* PHPUnit: 9.6.36 (45 tests)
= [4.3.1] - 2026-01-20 = = [5.1.4] - 2026-08-22 =
**Fixed**
* Dashboard widget now correctly counts only vulnerabilities from enabled components, excluding disabled ones from settings.
* Status badge calculation (Critical/Warning) now properly considers only enabled components when determining severity level.
* Fixed PHPCS warnings for global variables without plugin prefix in wpvulnerability-admin.php and wpvulnerability-adminms.php.
**Compatibility**
* WordPress: 4.7 - 6.9
* PHP: 5.6 - 8.5
* WP-CLI: 2.3.0 - 2.11.0
**Tests**
* PHP Coding Standards: 3.13.5
* WordPress Coding Standards: 3.3.0
* Plugin Check (PCP): 1.8.0
= [4.3.0] - 2026-01-19 =
**Highlights** **Highlights**
This major release brings a complete redesign of the admin interface, comprehensive debugging tools, enhanced security, and new notification channels. * The Site Health vulnerability tests work again on every WordPress version: they were silently disabled everywhere by an availability gate that could never pass. Secrets are now masked in the admin forms, and compatibility was verified against real PHP interpreters from 5.6 to 8.5.
* **Complete Admin Interface Redesign**: Modern card-based layout with visual component cards, improved spacing, color-coded status badges, and responsive grid layouts across all Settings tabs (Security, About, Logs, Tools, Analysis, and Notifications). **Fixed**
* **New Debug Mode**: Comprehensive debugging tools visible when WP_DEBUG is enabled, including System Information, Component Detection with cache status, interactive API Testing with AJAX buttons, Configuration Summary, Cron Status with manual execution, Database Options Viewer, and Quick Actions (clear caches, reset signatures, export debug data as JSON). Includes enhanced web server detection for 9 different servers.
* **Dashboard Widget Redesign**: Modern interface with prominent status badges (All Clear/Issues Found/Critical Issues Found), last check timestamp with human-readable format, empty state display with centered checkmark, separated component sections (WordPress Components vs Server Software), color-coded badges, and 2-column grid layout for server software.
* **New Notification Channels**: Discord webhook support and Telegram bot integration for vulnerability notifications, with proper character limit handling and webhook URL validation.
* **Enhanced Security**: CSRF protection via nonces for plugin/theme vulnerability filters, hybrid software detection system (PHP extensions first, shell commands as fallback), complete audit logging for shell executions, webhook URL validation with HTTPS enforcement and domain whitelisting, and new wp-config.php security constants.
* **Multisite Improvements**: Fixed all form submission issues in Network Admin, proper nonce handling, correct URL generation with network_admin_url(), preserved settings when saving individual forms, and fixed Security tab with all required helper functions.
* **Tools & Maintenance**: Cron event repair functionality for single sites and multisite networks, "Delete all logs" button, full plugin reset action, and improved cache management.
**Added** * The Site Health integration registered its tests behind `class_exists( 'WP_Site_Health' )`, but core loads that class lazily, after `init`, where the plugin loads its modules — so the guard always evaluated to false and none of the 13 vulnerability tests ever appeared in Site Health. The filter is now registered unconditionally; below WordPress 5.2 it simply never fires.
* "Clear all caches" in the debug tools now also deletes the `wpvulnerability-plugins-cache-data` timestamp option, so the plugins dataset is fully refreshed afterwards.
* Tools tab now lists expected WPVulnerability cron events, shows the scheduled instances, and lets administrators repair them on single sites. * The notification email "From:" address is now sanitized with `sanitize_email()` on all code paths.
* Network Tools highlights unexpected WPVulnerability cron events on subsites and provides a one-click repair to clear and reschedule across the network. * The plugins list table "Last updated" column decoded the whole plugins dataset for every table row; it is now decoded once per request.
* Logs tab now includes a "Delete all logs" button to purge stored API log entries manually. * Removed a duplicated `json_decode()` call when reading the vulnerable-themes count.
* Hybrid software detection system: PHP extensions first (most secure), shell commands fallback (most accurate). * The `wp_date()` compatibility polyfill no longer fatals on WordPress 4.75.3: core calls `wp_maybe_decline_date()` unconditionally, but that function only exists since 5.4; the call is now availability-guarded.
* Four-level security control system for shell_exec usage with granular configuration. * The core and software JSON getters return `'[]'` instead of an empty string when encoding fails, matching the plugins and themes getters.
* Shell command validation with whitelist and dangerous pattern detection.
* Complete audit logging for all shell executions (component, command, output, user, IP, timestamp).
* New wp-config.php constants: `WPVULNERABILITY_DISABLE_SHELL_EXEC`, `WPVULNERABILITY_SECURITY_MODE`, `WPVULNERABILITY_SHELL_EXEC_WHITELIST`.
* Detection reliability scoring (0-100) for each software detection method.
* Discord webhook support for vulnerability notifications with 2000-character limit handling.
* Telegram bot support for vulnerability notifications with bot token and chat ID configuration.
* Documentation links for Discord webhook setup (https://support.discord.com/hc/articles/228383668).
* Documentation links for Telegram bot setup (https://core.telegram.org/bots).
* Debug tab (visible only when WP_DEBUG is enabled) providing comprehensive debugging tools for administrators.
* wpvulnerability-debug.php file with 11 helper functions for debug functionality (~18KB).
* Debug Section 1: System Information displaying WordPress version, PHP version/extensions/memory, database type (MariaDB vs MySQL with proper detection), web server (nginx/Apache/LiteSpeed/OpenLiteSpeed/Caddy/IIS/Angie/OpenResty/Tengine), debug modes status, and debug log file information.
* Debug log file information showing file path, size, and direct browser link when web-accessible.
* Debug Section 2: Component Detection table listing all 13 components with detection status, version, analyzed state, and cache expiration time.
* Debug Section 3: API Testing with interactive AJAX-powered buttons to test API connectivity for each component individually, showing HTTP status, response time, and data preview.
* Debug Section 4: Configuration Summary displaying current plugin settings at a glance.
* Debug Section 5: Cron Status showing scheduled tasks with manual execution buttons (permission-gated with nonce verification).
* Debug Section 6: Database Options Viewer with dropdown to inspect all stored plugin options.
* Debug Section 7: Quick Actions with buttons to clear all caches, reset plugin/theme signatures, and export debug information as JSON.
* Enhanced web server detection supporting 9 different servers: nginx, Angie (nginx fork), Apache, LiteSpeed, OpenLiteSpeed, Caddy, IIS, OpenResty (nginx-based), and Tengine (nginx fork).
* Three-level detection logic for web servers: (1) plugin's standard function, (2) SERVER_SOFTWARE parsing, (3) shell commands as fallback.
* Database detection properly distinguishes MariaDB from MySQL via version_comment SQL query.
* AJAX handler `wpvulnerability_ajax_test_api()` for API testing with nonce verification and capability checks.
* Debug helper functions: `wpvulnerability_debug_get_log_file_info()`, `wpvulnerability_debug_detect_webserver()`, `wpvulnerability_debug_get_system_info()`, `wpvulnerability_debug_get_component_status()`, `wpvulnerability_debug_test_api_component()`, `wpvulnerability_debug_get_cron_status()`, `wpvulnerability_debug_export_info()`, `wpvulnerability_debug_clear_all_caches()`, `wpvulnerability_debug_reset_signatures()`, `wpvulnerability_debug_get_option_names()`.
* Debug tab POST handlers in both wpvulnerability-admin.php and wpvulnerability-adminms.php with nonce verification for all actions.
* Debug tab render functions in wpvulnerability-admin.php: `wpvulnerability_render_admin_tab_debug()`, `wpvulnerability_render_debug_section_system_info()`, `wpvulnerability_render_debug_section_components()`, `wpvulnerability_render_debug_section_config()`, `wpvulnerability_render_debug_section_cron()`, `wpvulnerability_render_debug_section_api_testing()`, `wpvulnerability_render_debug_section_database_options()`, `wpvulnerability_render_debug_section_quick_actions()`.
**Security** **Security**
* Added CSRF protection via nonces for plugin vulnerability filter links to prevent forced navigation attacks. * Webhook URLs (Slack, Teams, Discord) and the Telegram bot token are masked in the admin settings forms (single-site and network), showing only their last 4 characters — previously they were rendered in full into the page HTML. The inputs are plain text fields, so masked values never trigger browser URL validation. Saving a form with unchanged masked values keeps the stored secret; clearing a field still removes it.
* Added CSRF protection via nonces for theme vulnerability filter links to prevent forced navigation attacks. * Removed the dead `register_uninstall_hook()` registration: `uninstall.php` supersedes it and the registration added a stray autoloaded option row.
* Plugin and theme filter URLs now include security nonces that are verified before applying filters. * Single-site settings registrations now explicitly set `show_in_rest => false`, matching the multisite configuration.
* Enhanced security against timing attacks on vulnerability enumeration by requiring valid nonces.
* Enhanced ImageMagick, Redis, Memcached, and SQLite detection with hybrid approach and security controls.
* All shell commands hardcoded and validated - no user input involved.
* Shell execution wrapper with complete security checks and logging.
* Webhook URLs now validated on save: enforces HTTPS and verifies hostname matches allowed domains (Slack: hooks.slack.com, Teams: office.com/office365.com/api.hooks.microsoft.com, Discord: discord.com/discordapp.com).
* Telegram bot token validation with regex pattern check (format: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11).
* Invalid webhook URLs and bot tokens are rejected with user-friendly error messages instead of silently failing on notification send.
* Security audit score improved from 85/100 to 98/100 (comprehensive security audit completed 2026-01-19).
**Changed** **Changed**
* Database error hook now fires the prefixed `wpvulnerability_wpdb_last_error` action while keeping the legacy hook deprecated. * Admin CSS/JS assets load only on the WPVulnerability settings pages, the dashboard and the plugins/themes list tables instead of on every wp-admin screen.
* Database update cron now follows the configured cache duration (hourly, 6h, 12h, or 24h). * The shell availability probe (`wpvulnerability_can_shell_exec()`) result is cached per request instead of spawning a process on every call.
* Translations load on init to avoid early textdomain warnings.
* Vulnerability data now refreshes only via scheduled cron or the manual reload action; admin requests, REST, CLI, notifications, and view rendering no longer trigger API calls to reduce load.
* Cron scheduling no longer re-queues the update event on every page load; it keeps the configured interval (hourly/6h/12h/24h) unless the schedule changes.
* REST API now requires administrator capabilities for session or application password access.
* Admin reset and test email actions enforce admin capabilities.
* API client timeouts corrected to 2.5 seconds to avoid long hangs.
* Slack/Teams webhooks restricted to allowed HTTPS hosts to limit SSRF exposure.
* Tools tab adds a full reset action that clears all plugin data, restores defaults, and reloads from the API.
* Complete redesign of the admin settings interface with modern card-based layout for all tabs: Security, About, Logs, Tools, Analysis, and Notifications.
* Settings tabs now feature visual component cards with icons, improved spacing, color-coded status badges, and responsive grid layouts for enhanced usability.
* Complete redesign of the Dashboard widget with modern, clean interface replacing the basic list layout.
* Dashboard widget now features prominent status badge with three states: "All Clear" (green, 0 vulnerabilities), "X Issues Found" (yellow, minor issues), "X Critical Issues Found" (red, Core/PHP vulnerable or 5+ total vulnerabilities).
* Dashboard widget displays last check timestamp with human-readable format ("X minutes ago", "X hours ago") and "Refresh Now" link.
* Dashboard widget shows attractive empty state with centered green checkmark when no vulnerabilities are detected.
* Dashboard widget separates components into two clear sections: "WordPress Components" (Core, Plugins, Themes) and "Server Software" (PHP, web server, database, etc.).
* Dashboard widget uses color-coded badges: green for secure (✓ 0), yellow/red for vulnerabilities (✕ N) with consistent design.
* Dashboard widget displays server software in 2-column grid layout for better space utilization.
* Dashboard widget shows vulnerable plugin/theme lists indented below their respective components.
* Dashboard widget uses inline CSS with modern styling: flexbox layouts, proper spacing, WordPress admin color palette, improved typography (13px components, 12px meta).
* Dashboard widget footer includes links to Site Health and Settings with visual separation.
* All CSS class names, HTML IDs, and JavaScript references changed from short prefix `.wpv-` to full prefix `.wpvulnerability-` to prevent naming conflicts with other plugins and themes (affects 73+ unique classes across admin panels).
* All inline CSS from admin panels moved to external assets/admin.css file for better performance and cacheability (~1084 lines extracted from 14 style blocks across single-site and multisite admin panels).
* License updated from GPL2+ to GPL3+.
**Fixed**
* Saving partial forms (e.g., log retention) no longer resets other notification settings to their defaults; existing values are preserved unless explicitly changed.
* Network (adminms) settings now preserve existing values when saving individual forms and keep notification scheduling consistent.
* Cache column in Component Detection (Debug tab) now correctly displays cache expiration time by properly decoding JSON-encoded timestamps from database.
* Database detection now properly distinguishes between MariaDB and MySQL instead of always showing "MySQL".
* Multisite network admin forms (Notifications, Analysis, Configuration) now submit correctly by using proper network admin URLs and nonces instead of incompatible Settings API methods.
* All form submission buttons in multisite admin now include proper `name` attribute (`wpvulnerability_submit`) for backend processing.
* Delete logs form in multisite now uses `network_admin_url()` instead of single-site `admin_url()` for correct URL generation.
* All multisite form nonce verifications now match their corresponding form nonce fields (reset, email, repair cron, delete logs) instead of using generic nonce.
* Security tab in multisite network admin now works correctly by including all required helper functions (wpvulnerability_display_security_status, wpvulnerability_display_detection_methods, wpvulnerability_display_security_logs) instead of calling undefined single-site functions.
* Tools tab buttons in multisite network admin no longer show "nonce expired" errors by submitting forms to the same page (action="") instead of explicit URL paths that fail referer validation.
* Delete All Logs button in Logs tab no longer shows "The link you followed has expired" error by using empty form action (action="") in both single-site and multisite admin panels.
* Logs tab pagination now displays horizontally instead of vertically by properly targeting list elements (ul with inline-flex) and individual page links (a and span within li) to work correctly with WordPress paginate_links() 'type' => 'list' output.
**Compatibility** **Compatibility**
* WordPress: 4.7 - 6.9 * WordPress: 4.7 - 7.1
* PHP: 5.6 - 8.5 * PHP: 7.0 - 8.5
* WP-CLI: 2.3.0 - 2.11.0 * WP-CLI: 2.3.0 - 2.12.0
* Verified against real PHP interpreters 5.6.40, 7.0.33, 7.1.33, 7.2.34, 7.3.33, 7.4.33, 8.0.30, 8.1.34, 8.2.33, 8.3.33, 8.4.24 and 8.5.9: every plugin file lints clean from 7.0 up and fails on 5.6, confirming the declared floor. PHPCompatibility gate 7.0-8.5 clean; wp-compat WordPress floor 4.7 clean.
**Tests** **Tests**
* PHP Coding Standards: 3.13.5 * PHP Coding Standards: 3.13.6
* WordPress Coding Standards: 3.3.0 * WordPress Coding Standards: 3.4.1
* Plugin Check (PCP): 1.8.0 * PHPStan: 2.2.9 (level 9, 0 errors)
* PHPUnit: 9.6.36 (45 tests)
= [4.2.2.1] - 2026-01-16 =
**Security**
* Fixed authorization vulnerability in REST API endpoints that allowed low-privileged users to access sensitive vulnerability data. API now properly verifies user capabilities (`manage_options` for single sites, `manage_network_options` for multisite) in addition to authentication.
* Added direct access protection to wpvulnerability-api.php to prevent direct file execution outside WordPress context.
**Compatibility**
* WordPress: 4.7 - 6.9
* PHP: 5.6 - 8.4
* WP-CLI: 2.3.0 - 2.11.0
**Tests**
* PHP Coding Standards: 3.13.5
* WordPress Coding Standards: 3.3.0
* Plugin Check (PCP): 1.8.0
= [4.2.1] - 2025-12-17 =
**Fixed**
* Clear legacy WPVulnerability cron events from multisite subsites so updates and log cleanup only run on the main site.
**Compatibility**
* WordPress: 4.7 - 6.9
* PHP: 5.6 - 8.4
* WP-CLI: 2.3.0 - 2.11.0
**Tests**
* PHP Coding Standards: 3.13.5
* WordPress Coding Standards: 3.3.0
* Plugin Check (PCP): 1.7.0
= Previous versions = = Previous versions =
@ -517,8 +312,8 @@ This plugin adheres to the following security measures and review protocols for
* A security vulnerability was found and fixed in version 4.2.2.1. All previous versions (3.3.0 - 4.2.1) are affected. Please update to version 4.2.2.1 or later. * A security vulnerability was found and fixed in version 4.2.2.1. All previous versions (3.3.0 - 4.2.1) are affected. Please update to version 4.2.2.1 or later.
Found a security vulnerability? Please report it to us privately at the [WPVulnerability GitHub repository](https://github.com/javiercasares/wpvulnerability/security/advisories/new). Found a security vulnerability? Please report it to us privately at [ROBOTSTXT](https://www.robotstxt.es/contacto/).
== Contributors == == Contributors ==
You can contribute to this plugin at the [WPVulnerability GitHub repository](https://github.com/javiercasares/wpvulnerability). You can contribute to this plugin at the [WPVulnerability repository](https://git.robotstxt.es/ROBOTSTXT/wpvulnerability).

View file

@ -39,6 +39,7 @@ if ( ! defined( 'WPVULNERABILITY_PLUGIN_BASE' ) ) {
} }
require_once WPVULNERABILITY_PLUGIN_PATH . 'wpvulnerability-run.php'; require_once WPVULNERABILITY_PLUGIN_PATH . 'wpvulnerability-run.php';
require_once WPVULNERABILITY_PLUGIN_PATH . 'wpvulnerability-general.php';
if ( function_exists( 'wpvulnerability_uninstall' ) ) { if ( function_exists( 'wpvulnerability_uninstall' ) ) {
wpvulnerability_uninstall(); wpvulnerability_uninstall();

View file

@ -19,14 +19,23 @@
$wpvulnerability_settings = get_option( 'wpvulnerability-config' ); $wpvulnerability_settings = get_option( 'wpvulnerability-config' );
$wpvulnerability_analyze = get_option( 'wpvulnerability-analyze' ); $wpvulnerability_analyze = get_option( 'wpvulnerability-analyze' );
/** /**
* Enqueues the WPVulnerability admin CSS file on WPVulnerability admin pages. * Enqueues the WPVulnerability admin CSS file on WPVulnerability admin pages.
* *
* @since 2.0.0 * @since 2.0.0
* * @since 5.1.4 Assets load only on the plugin pages and the dashboard.
* @return void *
*/ * @param string $hook The current admin page hook.
function wpvulnerability_admin_enqueue_scripts() { *
* @return void
*/
function wpvulnerability_admin_enqueue_scripts( $hook ) {
// Load on the settings page, the dashboard (widget) and the list tables
// where vulnerability columns are rendered.
if ( ! in_array( $hook, array( 'index.php', 'settings_page_wpvulnerability-options', 'plugins.php', 'themes.php' ), true ) ) {
return;
}
wp_enqueue_style( wp_enqueue_style(
'wpvulnerability-admin', 'wpvulnerability-admin',
WPVULNERABILITY_PLUGIN_URL . 'assets/admin.css', WPVULNERABILITY_PLUGIN_URL . 'assets/admin.css',
@ -143,7 +152,7 @@ if ( isset( $_POST['wpvulnerability_delete_on_uninstall'] ) && check_admin_refer
} }
$wpvulnerability_settings['delete_on_uninstall'] = isset( $_POST['delete_on_uninstall'] ) ? 1 : 0; $wpvulnerability_settings['delete_on_uninstall'] = isset( $_POST['delete_on_uninstall'] ) ? 1 : 0;
update_option( 'wpvulnerability-config', $wpvulnerability_settings ); update_option( 'wpvulnerability-config', $wpvulnerability_settings );
add_settings_error( 'wpvulnerability-tools', 'uninstall-saved', __( 'Uninstall preference saved.', 'wpvulnerability' ), 'success' ); set_transient( 'wpvulnerability_message_manual_success', __( 'Uninstall preference saved.', 'wpvulnerability' ), 10 );
} }
} }
@ -532,6 +541,7 @@ function wpvulnerability_render_admin_tab_notifications() {
</div> </div>
<div class="wpvulnerability-checkbox-group"> <div class="wpvulnerability-checkbox-group">
<label> <label>
<input type="hidden" name="wpvulnerability-config[notify][email]" value="n" />
<input type="checkbox" name="wpvulnerability-config[notify][email]" value="y" <?php checked( $email_enabled ); ?> onchange="wpvToggleChannelInput('email')" /> <input type="checkbox" name="wpvulnerability-config[notify][email]" value="y" <?php checked( $email_enabled ); ?> onchange="wpvToggleChannelInput('email')" />
<?php esc_html_e( 'Email', 'wpvulnerability' ); ?> <?php esc_html_e( 'Email', 'wpvulnerability' ); ?>
</label> </label>
@ -542,12 +552,13 @@ function wpvulnerability_render_admin_tab_notifications() {
</div> </div>
<label> <label>
<input type="hidden" name="wpvulnerability-config[notify][slack]" value="n" />
<input type="checkbox" name="wpvulnerability-config[notify][slack]" value="y" <?php checked( $slack_enabled ); ?> onchange="wpvToggleChannelInput('slack')" /> <input type="checkbox" name="wpvulnerability-config[notify][slack]" value="y" <?php checked( $slack_enabled ); ?> onchange="wpvToggleChannelInput('slack')" />
<?php esc_html_e( 'Slack', 'wpvulnerability' ); ?> <?php esc_html_e( 'Slack', 'wpvulnerability' ); ?>
</label> </label>
<div class="wpvulnerability-channel-inputs" id="wpvulnerability-slack-inputs"> <div class="wpvulnerability-channel-inputs" id="wpvulnerability-slack-inputs">
<label for="wpvulnerability_slack_webhook"><?php esc_html_e( 'Slack Webhook URL:', 'wpvulnerability' ); ?></label> <label for="wpvulnerability_slack_webhook"><?php esc_html_e( 'Slack Webhook URL:', 'wpvulnerability' ); ?></label>
<input class="wpvulnerability-input-full" type="url" name="wpvulnerability-config[slack_webhook]" id="wpvulnerability_slack_webhook" placeholder="https://hooks.slack.com/services/..." value="<?php echo esc_attr( (string) $wpvulnerability_settings['slack_webhook'] ); ?>" /> <input class="wpvulnerability-input-full" type="text" name="wpvulnerability-config[slack_webhook]" id="wpvulnerability_slack_webhook" placeholder="https://hooks.slack.com/services/..." value="<?php echo esc_attr( wpvulnerability_mask_secret( is_scalar( $wpvulnerability_settings['slack_webhook'] ) ? (string) $wpvulnerability_settings['slack_webhook'] : '' ) ); ?>" />
<span class="wpvulnerability-input-hint"> <span class="wpvulnerability-input-hint">
<?php <?php
printf( printf(
@ -560,12 +571,13 @@ function wpvulnerability_render_admin_tab_notifications() {
</div> </div>
<label> <label>
<input type="hidden" name="wpvulnerability-config[notify][teams]" value="n" />
<input type="checkbox" name="wpvulnerability-config[notify][teams]" value="y" <?php checked( $teams_enabled ); ?> onchange="wpvToggleChannelInput('teams')" /> <input type="checkbox" name="wpvulnerability-config[notify][teams]" value="y" <?php checked( $teams_enabled ); ?> onchange="wpvToggleChannelInput('teams')" />
<?php esc_html_e( 'Microsoft Teams', 'wpvulnerability' ); ?> <?php esc_html_e( 'Microsoft Teams', 'wpvulnerability' ); ?>
</label> </label>
<div class="wpvulnerability-channel-inputs" id="wpvulnerability-teams-inputs"> <div class="wpvulnerability-channel-inputs" id="wpvulnerability-teams-inputs">
<label for="wpvulnerability_teams_webhook"><?php esc_html_e( 'Teams Webhook URL:', 'wpvulnerability' ); ?></label> <label for="wpvulnerability_teams_webhook"><?php esc_html_e( 'Teams Webhook URL:', 'wpvulnerability' ); ?></label>
<input class="wpvulnerability-input-full" type="url" name="wpvulnerability-config[teams_webhook]" id="wpvulnerability_teams_webhook" placeholder="https://outlook.office.com/webhook/..." value="<?php echo esc_attr( (string) $wpvulnerability_settings['teams_webhook'] ); ?>" /> <input class="wpvulnerability-input-full" type="text" name="wpvulnerability-config[teams_webhook]" id="wpvulnerability_teams_webhook" placeholder="https://outlook.office.com/webhook/..." value="<?php echo esc_attr( wpvulnerability_mask_secret( is_scalar( $wpvulnerability_settings['teams_webhook'] ) ? (string) $wpvulnerability_settings['teams_webhook'] : '' ) ); ?>" />
<span class="wpvulnerability-input-hint"> <span class="wpvulnerability-input-hint">
<?php <?php
printf( printf(
@ -578,12 +590,13 @@ function wpvulnerability_render_admin_tab_notifications() {
</div> </div>
<label> <label>
<input type="hidden" name="wpvulnerability-config[notify][discord]" value="n" />
<input type="checkbox" name="wpvulnerability-config[notify][discord]" value="y" <?php checked( $discord_enabled ); ?> onchange="wpvToggleChannelInput('discord')" /> <input type="checkbox" name="wpvulnerability-config[notify][discord]" value="y" <?php checked( $discord_enabled ); ?> onchange="wpvToggleChannelInput('discord')" />
<?php esc_html_e( 'Discord', 'wpvulnerability' ); ?> <?php esc_html_e( 'Discord', 'wpvulnerability' ); ?>
</label> </label>
<div class="wpvulnerability-channel-inputs" id="wpvulnerability-discord-inputs"> <div class="wpvulnerability-channel-inputs" id="wpvulnerability-discord-inputs">
<label for="wpvulnerability_discord_webhook"><?php esc_html_e( 'Discord Webhook URL:', 'wpvulnerability' ); ?></label> <label for="wpvulnerability_discord_webhook"><?php esc_html_e( 'Discord Webhook URL:', 'wpvulnerability' ); ?></label>
<input class="wpvulnerability-input-full" type="url" name="wpvulnerability-config[discord_webhook]" id="wpvulnerability_discord_webhook" placeholder="https://discord.com/api/webhooks/..." value="<?php echo esc_attr( (string) $wpvulnerability_settings['discord_webhook'] ); ?>" /> <input class="wpvulnerability-input-full" type="text" name="wpvulnerability-config[discord_webhook]" id="wpvulnerability_discord_webhook" placeholder="https://discord.com/api/webhooks/..." value="<?php echo esc_attr( wpvulnerability_mask_secret( is_scalar( $wpvulnerability_settings['discord_webhook'] ) ? (string) $wpvulnerability_settings['discord_webhook'] : '' ) ); ?>" />
<span class="wpvulnerability-input-hint"> <span class="wpvulnerability-input-hint">
<?php <?php
printf( printf(
@ -596,12 +609,13 @@ function wpvulnerability_render_admin_tab_notifications() {
</div> </div>
<label> <label>
<input type="hidden" name="wpvulnerability-config[notify][telegram]" value="n" />
<input type="checkbox" name="wpvulnerability-config[notify][telegram]" value="y" <?php checked( $telegram_enabled ); ?> onchange="wpvToggleChannelInput('telegram')" /> <input type="checkbox" name="wpvulnerability-config[notify][telegram]" value="y" <?php checked( $telegram_enabled ); ?> onchange="wpvToggleChannelInput('telegram')" />
<?php esc_html_e( 'Telegram', 'wpvulnerability' ); ?> <?php esc_html_e( 'Telegram', 'wpvulnerability' ); ?>
</label> </label>
<div class="wpvulnerability-channel-inputs" id="wpvulnerability-telegram-inputs"> <div class="wpvulnerability-channel-inputs" id="wpvulnerability-telegram-inputs">
<label for="wpvulnerability_telegram_bot_token"><?php esc_html_e( 'Telegram Bot Token:', 'wpvulnerability' ); ?></label> <label for="wpvulnerability_telegram_bot_token"><?php esc_html_e( 'Telegram Bot Token:', 'wpvulnerability' ); ?></label>
<input class="wpvulnerability-input-full" type="text" name="wpvulnerability-config[telegram_bot_token]" id="wpvulnerability_telegram_bot_token" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" value="<?php echo esc_attr( (string) $wpvulnerability_settings['telegram_bot_token'] ); ?>" /> <input class="wpvulnerability-input-full" type="text" name="wpvulnerability-config[telegram_bot_token]" id="wpvulnerability_telegram_bot_token" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" value="<?php echo esc_attr( wpvulnerability_mask_secret( is_scalar( $wpvulnerability_settings['telegram_bot_token'] ) ? (string) $wpvulnerability_settings['telegram_bot_token'] : '' ) ); ?>" />
<span class="wpvulnerability-input-hint"><?php esc_html_e( 'Format: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11', 'wpvulnerability' ); ?></span> <span class="wpvulnerability-input-hint"><?php esc_html_e( 'Format: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11', 'wpvulnerability' ); ?></span>
<label for="wpvulnerability_telegram_chat_id" style="margin-top: 12px;"><?php esc_html_e( 'Telegram Chat ID:', 'wpvulnerability' ); ?></label> <label for="wpvulnerability_telegram_chat_id" style="margin-top: 12px;"><?php esc_html_e( 'Telegram Chat ID:', 'wpvulnerability' ); ?></label>
@ -644,7 +658,7 @@ function wpvulnerability_render_admin_tab_notifications() {
} }
function wpvToggleChannelInput(channel) { function wpvToggleChannelInput(channel) {
var checkbox = document.querySelector('input[name="wpvulnerability-config[notify][' + channel + ']"]'); var checkbox = document.querySelector('input[type="checkbox"][name="wpvulnerability-config[notify][' + channel + ']"]');
var inputs = document.getElementById('wpvulnerability-' + channel + '-inputs'); var inputs = document.getElementById('wpvulnerability-' + channel + '-inputs');
if (checkbox.checked) { if (checkbox.checked) {
@ -1437,7 +1451,6 @@ function wpvulnerability_render_admin_tab_tools() {
<strong style="color: #b32d2e;"><?php esc_html_e( 'This action cannot be undone.', 'wpvulnerability' ); ?></strong> <strong style="color: #b32d2e;"><?php esc_html_e( 'This action cannot be undone.', 'wpvulnerability' ); ?></strong>
</p> </p>
<form method="post" action="<?php echo esc_url( $tools_action ); ?>" onsubmit="return confirm('<?php echo esc_js( __( 'Are you sure you want to reset all WPVulnerability data? This action cannot be undone.', 'wpvulnerability' ) ); ?>');"> <form method="post" action="<?php echo esc_url( $tools_action ); ?>" onsubmit="return confirm('<?php echo esc_js( __( 'Are you sure you want to reset all WPVulnerability data? This action cannot be undone.', 'wpvulnerability' ) ); ?>');">
<?php wp_nonce_field( 'wpvulnerability_reset_action', 'wpvulnerability_reset_nonce' ); ?>
<?php wp_nonce_field( 'wpvulnerability_full_reset_action', 'wpvulnerability_full_reset_nonce' ); ?> <?php wp_nonce_field( 'wpvulnerability_full_reset_action', 'wpvulnerability_full_reset_nonce' ); ?>
<input type="submit" name="wpvulnerability_full_reset" value="<?php esc_attr_e( 'Reset Plugin', 'wpvulnerability' ); ?>" class="button button-delete"> <input type="submit" name="wpvulnerability_full_reset" value="<?php esc_attr_e( 'Reset Plugin', 'wpvulnerability' ); ?>" class="button button-delete">
</form> </form>
@ -2041,8 +2054,8 @@ function wpvulnerability_admin_slack_callback() {
$slack_webhook = isset( $wpvulnerability_settings['slack_webhook'] ) ? $wpvulnerability_settings['slack_webhook'] : ''; $slack_webhook = isset( $wpvulnerability_settings['slack_webhook'] ) ? $wpvulnerability_settings['slack_webhook'] : '';
?> ?>
<input class="regular-text" type="text" name="wpvulnerability-config[slack_webhook]" id="wpvulnerability_slack_webhook" placeholder="<?php echo esc_attr( 'https://hooks.slack.com/services/...' ); ?>" value="<?php echo esc_attr( is_scalar( $slack_webhook ) ? (string) $slack_webhook : '' ); ?>" /> <input class="regular-text" type="text" name="wpvulnerability-config[slack_webhook]" id="wpvulnerability_slack_webhook" placeholder="<?php echo esc_attr( 'https://hooks.slack.com/services/...' ); ?>" value="<?php echo esc_attr( wpvulnerability_mask_secret( is_scalar( $slack_webhook ) ? (string) $slack_webhook : '' ) ); ?>" />
<?php <?php
} }
/** /**
@ -2061,8 +2074,8 @@ function wpvulnerability_admin_teams_callback() {
$teams_webhook = isset( $wpvulnerability_settings['teams_webhook'] ) ? $wpvulnerability_settings['teams_webhook'] : ''; $teams_webhook = isset( $wpvulnerability_settings['teams_webhook'] ) ? $wpvulnerability_settings['teams_webhook'] : '';
?> ?>
<input class="regular-text" type="text" name="wpvulnerability-config[teams_webhook]" id="wpvulnerability_teams_webhook" placeholder="<?php echo esc_attr( 'https://outlook.office.com/webhook/...' ); ?>" value="<?php echo esc_attr( is_scalar( $teams_webhook ) ? (string) $teams_webhook : '' ); ?>" /> <input class="regular-text" type="text" name="wpvulnerability-config[teams_webhook]" id="wpvulnerability_teams_webhook" placeholder="<?php echo esc_attr( 'https://outlook.office.com/webhook/...' ); ?>" value="<?php echo esc_attr( wpvulnerability_mask_secret( is_scalar( $teams_webhook ) ? (string) $teams_webhook : '' ) ); ?>" />
<?php <?php
} }
/** /**
@ -2211,7 +2224,7 @@ function wpvulnerability_admin_sanitize( $input ) {
foreach ( $input_email_text as $input_email ) { foreach ( $input_email_text as $input_email ) {
$input_email = sanitize_email( trim( (string) $input_email ) ); $input_email = sanitize_email( trim( (string) $input_email ) );
if ( $input_email ) { if ( is_email( $input_email ) ) {
$input_emails[] = $input_email; $input_emails[] = $input_email;
} }
} }
@ -2280,8 +2293,12 @@ function wpvulnerability_admin_sanitize( $input ) {
} }
if ( isset( $input['slack_webhook'] ) ) { if ( isset( $input['slack_webhook'] ) ) {
$slack_url = trim( is_scalar( $input['slack_webhook'] ) ? (string) $input['slack_webhook'] : '' ); $stored_slack = isset( $current_values['slack_webhook'] ) && is_scalar( $current_values['slack_webhook'] ) ? (string) $current_values['slack_webhook'] : '';
if ( '' !== $slack_url ) { $slack_url = trim( is_scalar( $input['slack_webhook'] ) ? (string) $input['slack_webhook'] : '' );
if ( '' !== $slack_url && wpvulnerability_mask_secret( $stored_slack ) === $slack_url ) {
// The field still holds the masked stored value: nothing changed.
$sanitized_values['slack_webhook'] = $stored_slack;
} elseif ( '' !== $slack_url ) {
$validated_slack = wpvulnerability_validate_webhook_url( $validated_slack = wpvulnerability_validate_webhook_url(
$slack_url, $slack_url,
array( 'hooks.slack.com' ) array( 'hooks.slack.com' )
@ -2303,8 +2320,12 @@ function wpvulnerability_admin_sanitize( $input ) {
} }
if ( isset( $input['teams_webhook'] ) ) { if ( isset( $input['teams_webhook'] ) ) {
$teams_url = trim( is_scalar( $input['teams_webhook'] ) ? (string) $input['teams_webhook'] : '' ); $stored_teams = isset( $current_values['teams_webhook'] ) && is_scalar( $current_values['teams_webhook'] ) ? (string) $current_values['teams_webhook'] : '';
if ( '' !== $teams_url ) { $teams_url = trim( is_scalar( $input['teams_webhook'] ) ? (string) $input['teams_webhook'] : '' );
if ( '' !== $teams_url && wpvulnerability_mask_secret( $stored_teams ) === $teams_url ) {
// The field still holds the masked stored value: nothing changed.
$sanitized_values['teams_webhook'] = $stored_teams;
} elseif ( '' !== $teams_url ) {
$validated_teams = wpvulnerability_validate_webhook_url( $validated_teams = wpvulnerability_validate_webhook_url(
$teams_url, $teams_url,
array( 'office.com', 'office365.com', 'api.hooks.microsoft.com' ) array( 'office.com', 'office365.com', 'api.hooks.microsoft.com' )
@ -2326,8 +2347,12 @@ function wpvulnerability_admin_sanitize( $input ) {
} }
if ( isset( $input['discord_webhook'] ) ) { if ( isset( $input['discord_webhook'] ) ) {
$discord_url = trim( is_scalar( $input['discord_webhook'] ) ? (string) $input['discord_webhook'] : '' ); $stored_discord = isset( $current_values['discord_webhook'] ) && is_scalar( $current_values['discord_webhook'] ) ? (string) $current_values['discord_webhook'] : '';
if ( '' !== $discord_url ) { $discord_url = trim( is_scalar( $input['discord_webhook'] ) ? (string) $input['discord_webhook'] : '' );
if ( '' !== $discord_url && wpvulnerability_mask_secret( $stored_discord ) === $discord_url ) {
// The field still holds the masked stored value: nothing changed.
$sanitized_values['discord_webhook'] = $stored_discord;
} elseif ( '' !== $discord_url ) {
$validated_discord = wpvulnerability_validate_webhook_url( $validated_discord = wpvulnerability_validate_webhook_url(
$discord_url, $discord_url,
array( 'discord.com', 'discordapp.com' ) array( 'discord.com', 'discordapp.com' )
@ -2349,8 +2374,12 @@ function wpvulnerability_admin_sanitize( $input ) {
} }
if ( isset( $input['telegram_bot_token'] ) ) { if ( isset( $input['telegram_bot_token'] ) ) {
$stored_tg_token = isset( $current_values['telegram_bot_token'] ) && is_scalar( $current_values['telegram_bot_token'] ) ? (string) $current_values['telegram_bot_token'] : '';
$telegram_bot_token = sanitize_text_field( trim( is_scalar( $input['telegram_bot_token'] ) ? (string) $input['telegram_bot_token'] : '' ) ); $telegram_bot_token = sanitize_text_field( trim( is_scalar( $input['telegram_bot_token'] ) ? (string) $input['telegram_bot_token'] : '' ) );
if ( '' !== $telegram_bot_token ) { if ( '' !== $telegram_bot_token && wpvulnerability_mask_secret( $stored_tg_token ) === $telegram_bot_token ) {
// The field still holds the masked stored value: nothing changed.
$sanitized_values['telegram_bot_token'] = $stored_tg_token;
} elseif ( '' !== $telegram_bot_token ) {
if ( ! preg_match( '/^\d+:[A-Za-z0-9_-]+$/', $telegram_bot_token ) ) { if ( ! preg_match( '/^\d+:[A-Za-z0-9_-]+$/', $telegram_bot_token ) ) {
add_settings_error( add_settings_error(
'wpvulnerability-config', 'wpvulnerability-config',
@ -2870,6 +2899,7 @@ function wpvulnerability_admin_init() {
array( array(
'sanitize_callback' => 'wpvulnerability_admin_sanitize', 'sanitize_callback' => 'wpvulnerability_admin_sanitize',
'default' => array(), 'default' => array(),
'show_in_rest' => false,
) )
); );
@ -2942,6 +2972,7 @@ function wpvulnerability_admin_init() {
array( array(
'sanitize_callback' => 'wpvulnerability_analyze_sanitize', 'sanitize_callback' => 'wpvulnerability_analyze_sanitize',
'default' => array(), 'default' => array(),
'show_in_rest' => false,
) )
); );
@ -3382,6 +3413,9 @@ function wpvulnerability_render_admin_tab_debug() {
// Section 2: Component Detection. // Section 2: Component Detection.
wpvulnerability_render_debug_section_components(); wpvulnerability_render_debug_section_components();
// Section 2b: PHP extensions and system packages.
wpvulnerability_render_debug_section_php_extensions();
// Section 4: Configuration Summary. // Section 4: Configuration Summary.
wpvulnerability_render_debug_section_config(); wpvulnerability_render_debug_section_config();
@ -3898,22 +3932,30 @@ function wpvulnerability_render_debug_section_api_testing() {
component: component, component: component,
nonce: '<?php echo esc_js( wp_create_nonce( 'wpvulnerability_test_api' ) ); ?>' nonce: '<?php echo esc_js( wp_create_nonce( 'wpvulnerability_test_api' ) ); ?>'
}, },
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
var result = response.data; var result = response.data;
var statusColor = result.success ? '#00a32a' : '#d63638'; var escHtml = function(str) {
var resultHtml = '<div style="border: 1px solid ' + statusColor + '; padding: 15px; margin-top: 10px; border-radius: 4px;">'; return String(str)
resultHtml += '<h4 style="margin-top: 0; color: ' + statusColor + ';">' + component.toUpperCase() + ' - ' + result.message + '</h4>'; .replace(/&/g, '&amp;')
resultHtml += '<p><strong><?php echo esc_js( __( 'HTTP Code:', 'wpvulnerability' ) ); ?></strong> ' + result.http_code + '</p>'; .replace(/</g, '&lt;')
resultHtml += '<p><strong><?php echo esc_js( __( 'Response Time:', 'wpvulnerability' ) ); ?></strong> ' + result.response_time + ' ms</p>'; .replace(/>/g, '&gt;')
if (result.data_preview) { .replace(/"/g, '&quot;')
resultHtml += '<details style="margin-top: 10px;"><summary style="cursor: pointer; font-weight: 600;"><?php echo esc_js( __( 'Response Preview', 'wpvulnerability' ) ); ?></summary>'; .replace(/'/g, '&#039;');
resultHtml += '<pre style="background: #f0f0f0; padding: 10px; overflow-x: auto; margin-top: 10px;">' + result.data_preview + '</pre>'; };
resultHtml += '</details>'; var statusColor = result.success ? '#00a32a' : '#d63638';
} var resultHtml = '<div style="border: 1px solid ' + statusColor + '; padding: 15px; margin-top: 10px; border-radius: 4px;">';
resultHtml += '</div>'; resultHtml += '<h4 style="margin-top: 0; color: ' + statusColor + ';">' + escHtml(component.toUpperCase()) + ' - ' + escHtml(result.message) + '</h4>';
$results.prepend(resultHtml); resultHtml += '<p><strong><?php echo esc_js( __( 'HTTP Code:', 'wpvulnerability' ) ); ?></strong> ' + escHtml(result.http_code) + '</p>';
resultHtml += '<p><strong><?php echo esc_js( __( 'Response Time:', 'wpvulnerability' ) ); ?></strong> ' + escHtml(result.response_time) + ' ms</p>';
if (result.data_preview) {
resultHtml += '<details style="margin-top: 10px;"><summary style="cursor: pointer; font-weight: 600;"><?php echo esc_js( __( 'Response Preview', 'wpvulnerability' ) ); ?></summary>';
resultHtml += '<pre style="background: #f0f0f0; padding: 10px; overflow-x: auto; margin-top: 10px;">' + escHtml(result.data_preview) + '</pre>';
resultHtml += '</details>';
} }
resultHtml += '</div>';
$results.prepend(resultHtml);
}
$btn.prop('disabled', false).text('<?php echo esc_js( __( 'Test', 'wpvulnerability' ) ); ?> ' + component.charAt(0).toUpperCase() + component.slice(1)); $btn.prop('disabled', false).text('<?php echo esc_js( __( 'Test', 'wpvulnerability' ) ); ?> ' + component.charAt(0).toUpperCase() + component.slice(1));
}, },
error: function() { error: function() {

View file

@ -23,10 +23,19 @@ $wpvulnerability_analyze = get_site_option( 'wpvulnerability-analyze' );
* Enqueues the WPVulnerability admin CSS file on WPVulnerability admin pages. * Enqueues the WPVulnerability admin CSS file on WPVulnerability admin pages.
* *
* @since 2.0.0 * @since 2.0.0
* @since 5.1.4 Assets load only on the plugin pages and the dashboard.
*
* @param string $hook The current admin page hook.
* *
* @return void * @return void
*/ */
function wpvulnerability_admin_enqueue_scripts() { function wpvulnerability_admin_enqueue_scripts( $hook ) {
// Load on the network settings page, the dashboard (widget) and the list
// tables where vulnerability columns are rendered.
if ( ! in_array( $hook, array( 'index.php', 'settings_page_wpvulnerability-options', 'plugins.php', 'themes.php' ), true ) ) {
return;
}
// Enqueue the admin stylesheet. // Enqueue the admin stylesheet.
wp_enqueue_style( wp_enqueue_style(
'wpvulnerability-admin', 'wpvulnerability-admin',
@ -53,8 +62,6 @@ function wpvulnerability_admin_enqueue_scripts() {
); );
} }
add_action( 'admin_enqueue_scripts', 'wpvulnerability_admin_enqueue_scripts' ); add_action( 'admin_enqueue_scripts', 'wpvulnerability_admin_enqueue_scripts' );
/** /**
* Processes the form submission for the WPVulnerability plugin settings in a multisite network admin context. * Processes the form submission for the WPVulnerability plugin settings in a multisite network admin context.
* *
@ -106,92 +113,11 @@ function wpvulnerability_process_network_config_forms() {
if ( isset( $_POST['wpvulnerability-analyze'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( isset( $_POST['wpvulnerability-analyze'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
$wpvulnerability_sanitized_values = array(
'core' => 0,
'plugins' => 0,
'themes' => 0,
'php' => 0,
'apache' => 0,
'nginx' => 0,
'mariadb' => 0,
'mysql' => 0,
'imagemagick' => 0,
'curl' => 0,
'memcached' => 0,
'redis' => 0,
'sqlite' => 0,
);
$wpvulnerability_analyze_raw = (array) wp_unslash( $_POST['wpvulnerability-analyze'] ); $wpvulnerability_analyze_raw = (array) wp_unslash( $_POST['wpvulnerability-analyze'] );
$wpvulnerability_values = array();
foreach ( $wpvulnerability_analyze_raw as $v ) {
$wpvulnerability_values[] = sanitize_text_field( is_scalar( $v ) ? (string) $v : '' );
}
foreach ( $wpvulnerability_values as $data ) { // Reuse the strict sanitizer so WPVULNERABILITY_HIDE_* constants
switch ( $data ) { // stay enforced on the network settings save path.
case 'core': update_site_option( 'wpvulnerability-analyze', wpvulnerability_sanitize_analyze( $wpvulnerability_analyze_raw ) );
$wpvulnerability_sanitized_values['core'] = 1;
break;
case 'plugins':
$wpvulnerability_sanitized_values['plugins'] = 1;
break;
case 'themes':
$wpvulnerability_sanitized_values['themes'] = 1;
break;
case 'php':
$wpvulnerability_sanitized_values['php'] = 1;
break;
case 'apache':
$wpvulnerability_sanitized_values['apache'] = 1;
break;
case 'nginx':
$wpvulnerability_sanitized_values['nginx'] = 1;
break;
case 'mariadb':
$wpvulnerability_sanitized_values['mariadb'] = 1;
break;
case 'mysql':
$wpvulnerability_sanitized_values['mysql'] = 1;
break;
case 'imagemagick':
$wpvulnerability_sanitized_values['imagemagick'] = 1;
break;
case 'curl':
$wpvulnerability_sanitized_values['curl'] = 1;
break;
case 'memcached':
$wpvulnerability_sanitized_values['memcached'] = 1;
break;
case 'redis':
$wpvulnerability_sanitized_values['redis'] = 1;
break;
case 'sqlite':
$wpvulnerability_sanitized_values['sqlite'] = 1;
break;
}
}
update_site_option(
'wpvulnerability-analyze',
array(
'core' => $wpvulnerability_sanitized_values['core'],
'plugins' => $wpvulnerability_sanitized_values['plugins'],
'themes' => $wpvulnerability_sanitized_values['themes'],
'php' => $wpvulnerability_sanitized_values['php'],
'apache' => $wpvulnerability_sanitized_values['apache'],
'nginx' => $wpvulnerability_sanitized_values['nginx'],
'mariadb' => $wpvulnerability_sanitized_values['mariadb'],
'mysql' => $wpvulnerability_sanitized_values['mysql'],
'imagemagick' => $wpvulnerability_sanitized_values['imagemagick'],
'curl' => $wpvulnerability_sanitized_values['curl'],
'memcached' => $wpvulnerability_sanitized_values['memcached'],
'redis' => $wpvulnerability_sanitized_values['redis'],
'sqlite' => $wpvulnerability_sanitized_values['sqlite'],
)
);
unset( $wpvulnerability_sanitized_values );
add_settings_error( add_settings_error(
'wpvulnerability-messages', 'wpvulnerability-messages',
@ -805,6 +731,7 @@ function wpvulnerability_render_network_admin_tab_notifications() {
</div> </div>
<div class="wpvulnerability-checkbox-group"> <div class="wpvulnerability-checkbox-group">
<label> <label>
<input type="hidden" name="wpvulnerability-config[notify][email]" value="n" />
<input type="checkbox" name="wpvulnerability-config[notify][email]" value="y" <?php checked( $email_enabled ); ?> onchange="wpvToggleChannelInput('email')" /> <input type="checkbox" name="wpvulnerability-config[notify][email]" value="y" <?php checked( $email_enabled ); ?> onchange="wpvToggleChannelInput('email')" />
<?php esc_html_e( 'Email', 'wpvulnerability' ); ?> <?php esc_html_e( 'Email', 'wpvulnerability' ); ?>
</label> </label>
@ -815,12 +742,13 @@ function wpvulnerability_render_network_admin_tab_notifications() {
</div> </div>
<label> <label>
<input type="hidden" name="wpvulnerability-config[notify][slack]" value="n" />
<input type="checkbox" name="wpvulnerability-config[notify][slack]" value="y" <?php checked( $slack_enabled ); ?> onchange="wpvToggleChannelInput('slack')" /> <input type="checkbox" name="wpvulnerability-config[notify][slack]" value="y" <?php checked( $slack_enabled ); ?> onchange="wpvToggleChannelInput('slack')" />
<?php esc_html_e( 'Slack', 'wpvulnerability' ); ?> <?php esc_html_e( 'Slack', 'wpvulnerability' ); ?>
</label> </label>
<div class="wpvulnerability-channel-inputs" id="wpvulnerability-slack-inputs"> <div class="wpvulnerability-channel-inputs" id="wpvulnerability-slack-inputs">
<label for="wpvulnerability_slack_webhook"><?php esc_html_e( 'Slack Webhook URL:', 'wpvulnerability' ); ?></label> <label for="wpvulnerability_slack_webhook"><?php esc_html_e( 'Slack Webhook URL:', 'wpvulnerability' ); ?></label>
<input class="wpvulnerability-input-full" type="url" name="wpvulnerability-config[slack_webhook]" id="wpvulnerability_slack_webhook" placeholder="https://hooks.slack.com/services/..." value="<?php echo esc_attr( (string) $wpvulnerability_settings['slack_webhook'] ); ?>" /> <input class="wpvulnerability-input-full" type="text" name="wpvulnerability-config[slack_webhook]" id="wpvulnerability_slack_webhook" placeholder="https://hooks.slack.com/services/..." value="<?php echo esc_attr( wpvulnerability_mask_secret( is_scalar( $wpvulnerability_settings['slack_webhook'] ) ? (string) $wpvulnerability_settings['slack_webhook'] : '' ) ); ?>" />
<span class="wpvulnerability-input-hint"> <span class="wpvulnerability-input-hint">
<?php <?php
printf( printf(
@ -833,12 +761,13 @@ function wpvulnerability_render_network_admin_tab_notifications() {
</div> </div>
<label> <label>
<input type="hidden" name="wpvulnerability-config[notify][teams]" value="n" />
<input type="checkbox" name="wpvulnerability-config[notify][teams]" value="y" <?php checked( $teams_enabled ); ?> onchange="wpvToggleChannelInput('teams')" /> <input type="checkbox" name="wpvulnerability-config[notify][teams]" value="y" <?php checked( $teams_enabled ); ?> onchange="wpvToggleChannelInput('teams')" />
<?php esc_html_e( 'Microsoft Teams', 'wpvulnerability' ); ?> <?php esc_html_e( 'Microsoft Teams', 'wpvulnerability' ); ?>
</label> </label>
<div class="wpvulnerability-channel-inputs" id="wpvulnerability-teams-inputs"> <div class="wpvulnerability-channel-inputs" id="wpvulnerability-teams-inputs">
<label for="wpvulnerability_teams_webhook"><?php esc_html_e( 'Teams Webhook URL:', 'wpvulnerability' ); ?></label> <label for="wpvulnerability_teams_webhook"><?php esc_html_e( 'Teams Webhook URL:', 'wpvulnerability' ); ?></label>
<input class="wpvulnerability-input-full" type="url" name="wpvulnerability-config[teams_webhook]" id="wpvulnerability_teams_webhook" placeholder="https://outlook.office.com/webhook/..." value="<?php echo esc_attr( (string) $wpvulnerability_settings['teams_webhook'] ); ?>" /> <input class="wpvulnerability-input-full" type="text" name="wpvulnerability-config[teams_webhook]" id="wpvulnerability_teams_webhook" placeholder="https://outlook.office.com/webhook/..." value="<?php echo esc_attr( wpvulnerability_mask_secret( is_scalar( $wpvulnerability_settings['teams_webhook'] ) ? (string) $wpvulnerability_settings['teams_webhook'] : '' ) ); ?>" />
<span class="wpvulnerability-input-hint"> <span class="wpvulnerability-input-hint">
<?php <?php
printf( printf(
@ -851,12 +780,13 @@ function wpvulnerability_render_network_admin_tab_notifications() {
</div> </div>
<label> <label>
<input type="hidden" name="wpvulnerability-config[notify][discord]" value="n" />
<input type="checkbox" name="wpvulnerability-config[notify][discord]" value="y" <?php checked( $discord_enabled ); ?> onchange="wpvToggleChannelInput('discord')" /> <input type="checkbox" name="wpvulnerability-config[notify][discord]" value="y" <?php checked( $discord_enabled ); ?> onchange="wpvToggleChannelInput('discord')" />
<?php esc_html_e( 'Discord', 'wpvulnerability' ); ?> <?php esc_html_e( 'Discord', 'wpvulnerability' ); ?>
</label> </label>
<div class="wpvulnerability-channel-inputs" id="wpvulnerability-discord-inputs"> <div class="wpvulnerability-channel-inputs" id="wpvulnerability-discord-inputs">
<label for="wpvulnerability_discord_webhook"><?php esc_html_e( 'Discord Webhook URL:', 'wpvulnerability' ); ?></label> <label for="wpvulnerability_discord_webhook"><?php esc_html_e( 'Discord Webhook URL:', 'wpvulnerability' ); ?></label>
<input class="wpvulnerability-input-full" type="url" name="wpvulnerability-config[discord_webhook]" id="wpvulnerability_discord_webhook" placeholder="https://discord.com/api/webhooks/..." value="<?php echo esc_attr( (string) $wpvulnerability_settings['discord_webhook'] ); ?>" /> <input class="wpvulnerability-input-full" type="text" name="wpvulnerability-config[discord_webhook]" id="wpvulnerability_discord_webhook" placeholder="https://discord.com/api/webhooks/..." value="<?php echo esc_attr( wpvulnerability_mask_secret( is_scalar( $wpvulnerability_settings['discord_webhook'] ) ? (string) $wpvulnerability_settings['discord_webhook'] : '' ) ); ?>" />
<span class="wpvulnerability-input-hint"> <span class="wpvulnerability-input-hint">
<?php <?php
printf( printf(
@ -869,12 +799,13 @@ function wpvulnerability_render_network_admin_tab_notifications() {
</div> </div>
<label> <label>
<input type="hidden" name="wpvulnerability-config[notify][telegram]" value="n" />
<input type="checkbox" name="wpvulnerability-config[notify][telegram]" value="y" <?php checked( $telegram_enabled ); ?> onchange="wpvToggleChannelInput('telegram')" /> <input type="checkbox" name="wpvulnerability-config[notify][telegram]" value="y" <?php checked( $telegram_enabled ); ?> onchange="wpvToggleChannelInput('telegram')" />
<?php esc_html_e( 'Telegram', 'wpvulnerability' ); ?> <?php esc_html_e( 'Telegram', 'wpvulnerability' ); ?>
</label> </label>
<div class="wpvulnerability-channel-inputs" id="wpvulnerability-telegram-inputs"> <div class="wpvulnerability-channel-inputs" id="wpvulnerability-telegram-inputs">
<label for="wpvulnerability_telegram_bot_token"><?php esc_html_e( 'Telegram Bot Token:', 'wpvulnerability' ); ?></label> <label for="wpvulnerability_telegram_bot_token"><?php esc_html_e( 'Telegram Bot Token:', 'wpvulnerability' ); ?></label>
<input class="wpvulnerability-input-full" type="text" name="wpvulnerability-config[telegram_bot_token]" id="wpvulnerability_telegram_bot_token" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" value="<?php echo esc_attr( (string) $wpvulnerability_settings['telegram_bot_token'] ); ?>" /> <input class="wpvulnerability-input-full" type="text" name="wpvulnerability-config[telegram_bot_token]" id="wpvulnerability_telegram_bot_token" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" value="<?php echo esc_attr( wpvulnerability_mask_secret( is_scalar( $wpvulnerability_settings['telegram_bot_token'] ) ? (string) $wpvulnerability_settings['telegram_bot_token'] : '' ) ); ?>" />
<span class="wpvulnerability-input-hint"><?php esc_html_e( 'Format: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11', 'wpvulnerability' ); ?></span> <span class="wpvulnerability-input-hint"><?php esc_html_e( 'Format: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11', 'wpvulnerability' ); ?></span>
<label for="wpvulnerability_telegram_chat_id" style="margin-top: 12px;"><?php esc_html_e( 'Telegram Chat ID:', 'wpvulnerability' ); ?></label> <label for="wpvulnerability_telegram_chat_id" style="margin-top: 12px;"><?php esc_html_e( 'Telegram Chat ID:', 'wpvulnerability' ); ?></label>
@ -917,7 +848,7 @@ function wpvulnerability_render_network_admin_tab_notifications() {
} }
function wpvToggleChannelInput(channel) { function wpvToggleChannelInput(channel) {
var checkbox = document.querySelector('input[name="wpvulnerability-config[notify][' + channel + ']"]'); var checkbox = document.querySelector('input[type="checkbox"][name="wpvulnerability-config[notify][' + channel + ']"]');
var inputs = document.getElementById('wpvulnerability-' + channel + '-inputs'); var inputs = document.getElementById('wpvulnerability-' + channel + '-inputs');
if (checkbox.checked) { if (checkbox.checked) {
@ -1447,14 +1378,6 @@ function wpvulnerability_render_network_admin_tab_logs() {
function wpvulnerability_render_network_admin_tab_tools() { function wpvulnerability_render_network_admin_tab_tools() {
global $wpvulnerability_settings; global $wpvulnerability_settings;
$tools_action = add_query_arg(
array(
'page' => 'wpvulnerability-options',
'tab' => 'tools',
),
network_admin_url( 'settings.php' )
);
$wpvulnerability_cron_config = is_multisite() ? get_site_option( 'wpvulnerability-config', array() ) : get_option( 'wpvulnerability-config', array() ); $wpvulnerability_cron_config = is_multisite() ? get_site_option( 'wpvulnerability-config', array() ) : get_option( 'wpvulnerability-config', array() );
if ( ! is_array( $wpvulnerability_cron_config ) ) { if ( ! is_array( $wpvulnerability_cron_config ) ) {
$wpvulnerability_cron_config = array(); $wpvulnerability_cron_config = array();
@ -2201,7 +2124,7 @@ function wpvulnerability_admin_slack_callback() {
$slack_webhook = isset( $wpvulnerability_settings['slack_webhook'] ) ? $wpvulnerability_settings['slack_webhook'] : ''; $slack_webhook = isset( $wpvulnerability_settings['slack_webhook'] ) ? $wpvulnerability_settings['slack_webhook'] : '';
?> ?>
<input class="regular-text" type="text" name="wpvulnerability-config[slack_webhook]" id="wpvulnerability_slack_webhook" placeholder="<?php echo esc_attr( 'https://hooks.slack.com/services/...' ); ?>" value="<?php echo esc_attr( is_scalar( $slack_webhook ) ? (string) $slack_webhook : '' ); ?>" /> <input class="regular-text" type="text" name="wpvulnerability-config[slack_webhook]" id="wpvulnerability_slack_webhook" placeholder="<?php echo esc_attr( 'https://hooks.slack.com/services/...' ); ?>" value="<?php echo esc_attr( wpvulnerability_mask_secret( is_scalar( $slack_webhook ) ? (string) $slack_webhook : '' ) ); ?>" />
<?php <?php
} }
@ -2221,7 +2144,7 @@ function wpvulnerability_admin_teams_callback() {
$teams_webhook = isset( $wpvulnerability_settings['teams_webhook'] ) ? $wpvulnerability_settings['teams_webhook'] : ''; $teams_webhook = isset( $wpvulnerability_settings['teams_webhook'] ) ? $wpvulnerability_settings['teams_webhook'] : '';
?> ?>
<input class="regular-text" type="text" name="wpvulnerability-config[teams_webhook]" id="wpvulnerability_teams_webhook" placeholder="<?php echo esc_attr( 'https://outlook.office.com/webhook/...' ); ?>" value="<?php echo esc_attr( is_scalar( $teams_webhook ) ? (string) $teams_webhook : '' ); ?>" /> <input class="regular-text" type="text" name="wpvulnerability-config[teams_webhook]" id="wpvulnerability_teams_webhook" placeholder="<?php echo esc_attr( 'https://outlook.office.com/webhook/...' ); ?>" value="<?php echo esc_attr( wpvulnerability_mask_secret( is_scalar( $teams_webhook ) ? (string) $teams_webhook : '' ) ); ?>" />
<?php <?php
} }
@ -2860,8 +2783,12 @@ function wpvulnerability_sanitize_config( $input ) {
// Webhooks. // Webhooks.
if ( isset( $input['slack_webhook'] ) ) { if ( isset( $input['slack_webhook'] ) ) {
$slack_url = trim( is_scalar( $input['slack_webhook'] ) ? (string) $input['slack_webhook'] : '' ); $stored_slack = isset( $current_values['slack_webhook'] ) && is_scalar( $current_values['slack_webhook'] ) ? (string) $current_values['slack_webhook'] : '';
if ( '' !== $slack_url ) { $slack_url = trim( is_scalar( $input['slack_webhook'] ) ? (string) $input['slack_webhook'] : '' );
if ( '' !== $slack_url && wpvulnerability_mask_secret( $stored_slack ) === $slack_url ) {
// The field still holds the masked stored value: nothing changed.
$sanitized['slack_webhook'] = $stored_slack;
} elseif ( '' !== $slack_url ) {
$validated_slack = wpvulnerability_validate_webhook_url( $validated_slack = wpvulnerability_validate_webhook_url(
$slack_url, $slack_url,
array( 'hooks.slack.com' ) array( 'hooks.slack.com' )
@ -2883,8 +2810,12 @@ function wpvulnerability_sanitize_config( $input ) {
} }
if ( isset( $input['teams_webhook'] ) ) { if ( isset( $input['teams_webhook'] ) ) {
$teams_url = trim( is_scalar( $input['teams_webhook'] ) ? (string) $input['teams_webhook'] : '' ); $stored_teams = isset( $current_values['teams_webhook'] ) && is_scalar( $current_values['teams_webhook'] ) ? (string) $current_values['teams_webhook'] : '';
if ( '' !== $teams_url ) { $teams_url = trim( is_scalar( $input['teams_webhook'] ) ? (string) $input['teams_webhook'] : '' );
if ( '' !== $teams_url && wpvulnerability_mask_secret( $stored_teams ) === $teams_url ) {
// The field still holds the masked stored value: nothing changed.
$sanitized['teams_webhook'] = $stored_teams;
} elseif ( '' !== $teams_url ) {
$validated_teams = wpvulnerability_validate_webhook_url( $validated_teams = wpvulnerability_validate_webhook_url(
$teams_url, $teams_url,
array( 'office.com', 'office365.com', 'api.hooks.microsoft.com' ) array( 'office.com', 'office365.com', 'api.hooks.microsoft.com' )
@ -2906,8 +2837,12 @@ function wpvulnerability_sanitize_config( $input ) {
} }
if ( isset( $input['discord_webhook'] ) ) { if ( isset( $input['discord_webhook'] ) ) {
$discord_url = trim( is_scalar( $input['discord_webhook'] ) ? (string) $input['discord_webhook'] : '' ); $stored_discord = isset( $current_values['discord_webhook'] ) && is_scalar( $current_values['discord_webhook'] ) ? (string) $current_values['discord_webhook'] : '';
if ( '' !== $discord_url ) { $discord_url = trim( is_scalar( $input['discord_webhook'] ) ? (string) $input['discord_webhook'] : '' );
if ( '' !== $discord_url && wpvulnerability_mask_secret( $stored_discord ) === $discord_url ) {
// The field still holds the masked stored value: nothing changed.
$sanitized['discord_webhook'] = $stored_discord;
} elseif ( '' !== $discord_url ) {
$validated_discord = wpvulnerability_validate_webhook_url( $validated_discord = wpvulnerability_validate_webhook_url(
$discord_url, $discord_url,
array( 'discord.com', 'discordapp.com' ) array( 'discord.com', 'discordapp.com' )
@ -2929,8 +2864,12 @@ function wpvulnerability_sanitize_config( $input ) {
} }
if ( isset( $input['telegram_bot_token'] ) ) { if ( isset( $input['telegram_bot_token'] ) ) {
$stored_tg_token = isset( $current_values['telegram_bot_token'] ) && is_scalar( $current_values['telegram_bot_token'] ) ? (string) $current_values['telegram_bot_token'] : '';
$telegram_bot_token = sanitize_text_field( trim( is_scalar( $input['telegram_bot_token'] ) ? (string) $input['telegram_bot_token'] : '' ) ); $telegram_bot_token = sanitize_text_field( trim( is_scalar( $input['telegram_bot_token'] ) ? (string) $input['telegram_bot_token'] : '' ) );
if ( '' !== $telegram_bot_token ) { if ( '' !== $telegram_bot_token && wpvulnerability_mask_secret( $stored_tg_token ) === $telegram_bot_token ) {
// The field still holds the masked stored value: nothing changed.
$sanitized['telegram_bot_token'] = $stored_tg_token;
} elseif ( '' !== $telegram_bot_token ) {
if ( ! preg_match( '/^\d+:[A-Za-z0-9_-]+$/', $telegram_bot_token ) ) { if ( ! preg_match( '/^\d+:[A-Za-z0-9_-]+$/', $telegram_bot_token ) ) {
add_settings_error( add_settings_error(
'wpvulnerability-config', 'wpvulnerability-config',
@ -2959,30 +2898,12 @@ function wpvulnerability_sanitize_config( $input ) {
return $sanitized; return $sanitized;
} }
/**
* Sanitizes the messages generated by the plugin (simple messages).
*
* @since 2.0.0
*
* @param array<string, mixed> $input Input values.
* @return array<string, string> Sanitized values.
*/
function wpvulnerability_sanitize_messages( $input ) {
$sanitized = array();
foreach ( $input as $key => $message ) {
$sanitized[ sanitize_key( (string) $key ) ] = sanitize_text_field( is_scalar( $message ) ? (string) $message : '' );
}
return $sanitized;
}
/** /**
* Strictly sanitizes the analysis options (booleans). * Strictly sanitizes the analysis options (booleans).
* *
* @since 3.3.0 * @since 3.3.0
* *
* @param array<string, mixed> $input Input values. * @param array<mixed, mixed> $input Input values.
* @return array<string, bool> Sanitized values. * @return array<string, bool> Sanitized values.
*/ */
function wpvulnerability_sanitize_analyze( $input ) { function wpvulnerability_sanitize_analyze( $input ) {
@ -3037,18 +2958,6 @@ function wpvulnerability_admin_init() {
) )
); );
// Register messages.
register_setting(
'admin_wpvulnerability_settings',
'wpvulnerability-messages',
array(
'sanitize_callback' => 'wpvulnerability_sanitize_messages',
'default' => array(),
'show_in_rest' => false,
'type' => 'array',
)
);
// Add a section to the settings page. // Add a section to the settings page.
add_settings_section( add_settings_section(
'admin_wpvulnerability_settings', 'admin_wpvulnerability_settings',
@ -3542,6 +3451,9 @@ function wpvulnerability_render_network_admin_tab_debug() {
// Section 2: Component Detection. // Section 2: Component Detection.
wpvulnerability_render_debug_section_components(); wpvulnerability_render_debug_section_components();
// Section 2b: PHP extensions and system packages.
wpvulnerability_render_debug_section_php_extensions();
// Section 4: Configuration Summary. // Section 4: Configuration Summary.
wpvulnerability_render_debug_section_config(); wpvulnerability_render_debug_section_config();
@ -4094,22 +4006,30 @@ function wpvulnerability_render_debug_section_api_testing() {
component: component, component: component,
nonce: '<?php echo esc_js( wp_create_nonce( 'wpvulnerability_test_api' ) ); ?>' nonce: '<?php echo esc_js( wp_create_nonce( 'wpvulnerability_test_api' ) ); ?>'
}, },
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
var result = response.data; var result = response.data;
var statusColor = result.success ? '#00a32a' : '#d63638'; var escHtml = function(str) {
var resultHtml = '<div style="border: 1px solid ' + statusColor + '; padding: 15px; margin-top: 10px; border-radius: 4px;">'; return String(str)
resultHtml += '<h4 style="margin-top: 0; color: ' + statusColor + ';">' + component.toUpperCase() + ' - ' + result.message + '</h4>'; .replace(/&/g, '&amp;')
resultHtml += '<p><strong><?php echo esc_js( __( 'HTTP Code:', 'wpvulnerability' ) ); ?></strong> ' + result.http_code + '</p>'; .replace(/</g, '&lt;')
resultHtml += '<p><strong><?php echo esc_js( __( 'Response Time:', 'wpvulnerability' ) ); ?></strong> ' + result.response_time + ' ms</p>'; .replace(/>/g, '&gt;')
if (result.data_preview) { .replace(/"/g, '&quot;')
resultHtml += '<details style="margin-top: 10px;"><summary style="cursor: pointer; font-weight: 600;"><?php echo esc_js( __( 'Response Preview', 'wpvulnerability' ) ); ?></summary>'; .replace(/'/g, '&#039;');
resultHtml += '<pre style="background: #f0f0f0; padding: 10px; overflow-x: auto; margin-top: 10px;">' + result.data_preview + '</pre>'; };
resultHtml += '</details>'; var statusColor = result.success ? '#00a32a' : '#d63638';
} var resultHtml = '<div style="border: 1px solid ' + statusColor + '; padding: 15px; margin-top: 10px; border-radius: 4px;">';
resultHtml += '</div>'; resultHtml += '<h4 style="margin-top: 0; color: ' + statusColor + ';">' + escHtml(component.toUpperCase()) + ' - ' + escHtml(result.message) + '</h4>';
$results.prepend(resultHtml); resultHtml += '<p><strong><?php echo esc_js( __( 'HTTP Code:', 'wpvulnerability' ) ); ?></strong> ' + escHtml(result.http_code) + '</p>';
resultHtml += '<p><strong><?php echo esc_js( __( 'Response Time:', 'wpvulnerability' ) ); ?></strong> ' + escHtml(result.response_time) + ' ms</p>';
if (result.data_preview) {
resultHtml += '<details style="margin-top: 10px;"><summary style="cursor: pointer; font-weight: 600;"><?php echo esc_js( __( 'Response Preview', 'wpvulnerability' ) ); ?></summary>';
resultHtml += '<pre style="background: #f0f0f0; padding: 10px; overflow-x: auto; margin-top: 10px;">' + escHtml(result.data_preview) + '</pre>';
resultHtml += '</details>';
} }
resultHtml += '</div>';
$results.prepend(resultHtml);
}
$btn.prop('disabled', false).text('<?php echo esc_js( __( 'Test', 'wpvulnerability' ) ); ?> ' + component.charAt(0).toUpperCase() + component.slice(1)); $btn.prop('disabled', false).text('<?php echo esc_js( __( 'Test', 'wpvulnerability' ) ); ?> ' + component.charAt(0).toUpperCase() + component.slice(1));
}, },
error: function() { error: function() {

View file

@ -557,8 +557,10 @@ function wpvulnerability_permission_check( WP_REST_Request $request ) {
return true; return true;
} }
// Check if application passwords are available. // Check if application passwords are available (WordPress 5.6+).
if ( wp_is_application_passwords_available() ) { // On older WordPress versions this authentication method is skipped and
// only cookie-authenticated sessions with admin capabilities are accepted.
if ( function_exists( 'wp_is_application_passwords_available' ) && function_exists( 'wp_authenticate_application_password' ) && wp_is_application_passwords_available() ) {
$authorization_header = $request->get_header( 'authorization' ); $authorization_header = $request->get_header( 'authorization' );
// Check if the authorization header is present and properly formatted. // Check if the authorization header is present and properly formatted.

372
wpvulnerability-compat.php Normal file
View file

@ -0,0 +1,372 @@
<?php
/**
* WordPress core function polyfills.
*
* Contains verbatim copies of small WordPress core functions, wrapped in
* function_exists() guards so they are only defined when the running
* WordPress version does not provide them. This keeps the plugin free of
* fatals on older WordPress versions.
*
* Policy (see docs/):
* - Plain FUNCTIONS are polyfilled here, copied from WordPress core as they are.
* - Whole FUNCTIONALITIES (Application Passwords since WP 5.6, Site Health
* since WP 5.2) are NOT polyfilled; their call sites are availability-gated
* and the plugin works without them.
*
* @package WPVulnerability
*
* @since 5.1.3
*/
defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
if ( ! function_exists( 'wp_doing_cron' ) ) {
/**
* Determines whether the current request is a WordPress cron request.
*
* @since 4.8.0
*
* @return bool True if it's a WordPress cron request, false otherwise.
*/
function wp_doing_cron() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Verbatim WordPress core polyfill.
/**
* Filters whether the current request is a WordPress cron request.
*
* @since 4.8.0
*
* @param bool $wp_doing_cron Whether the current request is a WordPress cron request.
*/
return apply_filters( 'wp_doing_cron', defined( 'DOING_CRON' ) && DOING_CRON ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Verbatim WordPress core polyfill; core hook names must be preserved.
}
}
if ( ! function_exists( 'get_main_site_id' ) ) {
/**
* Gets the main site ID.
*
* @since 4.9.0
*
* @param int|null $network_id Optional. The ID of the network for which to get the main site.
* Defaults to the current network.
* @return int The ID of the main site.
*/
function get_main_site_id( $network_id = null ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Verbatim WordPress core polyfill.
if ( ! is_multisite() ) {
return get_current_blog_id();
}
$network = get_network( $network_id );
if ( ! $network ) {
return 0;
}
return $network->site_id;
}
}
if ( ! function_exists( 'wp_is_json_request' ) ) {
/**
* Checks whether current request is a JSON request, or is expecting a JSON response.
*
* @since 5.0.0
*
* @return bool True if Accepts or Content-Type headers contain application/json.
* False otherwise.
*/
function wp_is_json_request() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Verbatim WordPress core polyfill.
if ( isset( $_SERVER['HTTP_CONTENT_TYPE'] ) && false !== strpos( $_SERVER['HTTP_CONTENT_TYPE'], 'application/json' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verbatim WordPress core polyfill; header used only for a strpos comparison.
return true;
}
if ( isset( $_SERVER['HTTP_ACCEPT'] ) && false !== strpos( $_SERVER['HTTP_ACCEPT'], 'application/json' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verbatim WordPress core polyfill; header used only for a strpos comparison.
return true;
}
return false;
}
}
if ( ! function_exists( 'sanitize_locale_name' ) ) {
/**
* Strips out all characters not allowed in a locale name.
*
* @since 6.2.1
*
* @param string $locale_name The locale name to be sanitized.
* @return string The sanitized value.
*/
function sanitize_locale_name( $locale_name ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Verbatim WordPress core polyfill.
// Limit to A-Z, a-z, 0-9, '_', '-'.
$sanitized = preg_replace( '/[^A-Za-z0-9_-]/', '', $locale_name );
/**
* Filters a sanitized locale name string.
*
* @since 6.2.1
*
* @param string $sanitized The sanitized locale name.
* @param string $locale_name The locale name before sanitization.
*/
return apply_filters( 'sanitize_locale_name', $sanitized, $locale_name ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Verbatim WordPress core polyfill; core hook names must be preserved.
}
}
if ( ! function_exists( 'determine_locale' ) ) {
/**
* Determines the current locale desired for the request.
*
* @since 5.0.0
*
* @global string $pagenow The filename of the current screen.
*
* @return string The determined locale.
*/
function determine_locale() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Verbatim WordPress core polyfill.
/**
* Filters the locale for the current request prior to the default determination process.
*
* Using this filter allows to override the default logic, effectively short-circuiting the function.
*
* @since 5.0.0
*
* @param string|null $locale The locale to return and short-circuit. Default null.
*/
$determined_locale = apply_filters( 'pre_determine_locale', null ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Verbatim WordPress core polyfill; core hook names must be preserved.
if ( $determined_locale && is_string( $determined_locale ) ) {
return $determined_locale;
}
$determined_locale = get_locale();
if ( is_admin() ) {
$determined_locale = get_user_locale();
}
if ( isset( $_GET['_locale'] ) && 'user' === $_GET['_locale'] && wp_is_json_request() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verbatim WordPress core polyfill; the parameter is only compared, never stored or displayed.
$determined_locale = get_user_locale();
}
if ( ! empty( $_GET['wp_lang'] ) && isset( $GLOBALS['pagenow'] ) && 'wp-login.php' === $GLOBALS['pagenow'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verbatim WordPress core polyfill; the parameter is sanitized below and never stored or displayed.
$determined_locale = sanitize_locale_name( wp_unslash( $_GET['wp_lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verbatim WordPress core polyfill; the parameter is sanitized before use and never stored or displayed.
}
/**
* Filters the locale for the current request.
*
* @since 5.0.0
*
* @param string $determined_locale The locale.
*/
return apply_filters( 'determine_locale', $determined_locale ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Verbatim WordPress core polyfill; core hook names must be preserved.
}
}
if ( ! function_exists( 'wp_timezone_string' ) ) {
/**
* Retrieves the timezone of the site as a string.
*
* @since 5.3.0
*
* @return string PHP timezone name or a ±HH:MM offset.
*/
function wp_timezone_string() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Verbatim WordPress core polyfill.
$timezone_string = get_option( 'timezone_string' );
if ( $timezone_string ) {
return $timezone_string;
}
$offset = (float) get_option( 'gmt_offset' );
$hours = (int) $offset;
$minutes = ( $offset - $hours );
$sign = ( $offset < 0 ) ? '-' : '+';
$abs_hour = abs( $hours );
$abs_mins = abs( $minutes * 60 );
$tz_offset = sprintf( '%s%02d:%02d', $sign, $abs_hour, $abs_mins );
return $tz_offset;
}
}
if ( ! function_exists( 'wp_timezone' ) ) {
/**
* Retrieves the timezone of the site as a DateTimeZone object.
*
* @since 5.3.0
*
* @return DateTimeZone Timezone object.
*/
function wp_timezone() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Verbatim WordPress core polyfill.
return new DateTimeZone( wp_timezone_string() );
}
}
if ( ! function_exists( 'wp_date' ) ) {
/**
* Retrieves the date, in localized format.
*
* @since 5.3.0
*
* @param string $format PHP date format.
* @param int|null $timestamp Optional. Unix timestamp. Defaults to current time.
* @param DateTimeZone|null $timezone Optional. Timezone to output result in.
* Defaults to timezone from site settings.
* @return string|false The date, translated if locale specifies it.
* False on invalid timestamp input.
*/
function wp_date( $format, $timestamp = null, $timezone = null ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Verbatim WordPress core polyfill.
global $wp_locale;
if ( null === $timestamp ) {
$timestamp = time();
} elseif ( ! is_numeric( $timestamp ) ) {
return false;
}
if ( ! $timezone ) {
$timezone = wp_timezone();
}
$datetime = date_create( '@' . $timestamp );
$datetime->setTimezone( $timezone );
if ( empty( $wp_locale->month ) || empty( $wp_locale->weekday ) ) {
$date = $datetime->format( $format );
} else {
// We need to unpack shorthand `r` format because it has parts that might be localized.
$format = preg_replace( '/(?<!\\\\)r/', DATE_RFC2822, $format );
$new_format = '';
$format_length = strlen( $format );
$month = $wp_locale->get_month( $datetime->format( 'm' ) );
$weekday = $wp_locale->get_weekday( $datetime->format( 'w' ) );
for ( $i = 0; $i < $format_length; $i++ ) {
switch ( $format[ $i ] ) {
case 'D':
$new_format .= addcslashes( $wp_locale->get_weekday_abbrev( $weekday ), '\\A..Za..z' );
break;
case 'F':
$new_format .= addcslashes( $month, '\\A..Za..z' );
break;
case 'l':
$new_format .= addcslashes( $weekday, '\\A..Za..z' );
break;
case 'M':
$new_format .= addcslashes( $wp_locale->get_month_abbrev( $month ), '\\A..Za..z' );
break;
case 'a':
$new_format .= addcslashes( $wp_locale->get_meridiem( $datetime->format( 'a' ) ), '\\A..Za..z' );
break;
case 'A':
$new_format .= addcslashes( $wp_locale->get_meridiem( $datetime->format( 'A' ) ), '\\A..Za..z' );
break;
case '\\':
$new_format .= $format[ $i ];
// If character follows a slash, we add it without translating.
if ( $i < $format_length ) {
$new_format .= $format[ ++$i ];
}
break;
default:
$new_format .= $format[ $i ];
break;
}
}
$date = $datetime->format( $new_format );
// Core's wp_date() calls wp_maybe_decline_date() unconditionally, but
// that function only exists since WP 5.4. Guarded here so the polyfill
// is safe on the declared 4.7 floor (dates are then not declined).
if ( function_exists( 'wp_maybe_decline_date' ) ) {
$date = wp_maybe_decline_date( $date );
}
}
/**
* Filters the date formatted based on the locale.
*
* @since 5.3.0
*
* @param string $date Formatted date string.
* @param string $format Format to display the date.
* @param int $timestamp Unix timestamp.
* @param DateTimeZone $timezone Timezone.
*/
$date = apply_filters( 'wp_date', $date, $format, $timestamp, $timezone ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Verbatim WordPress core polyfill; core hook names must be preserved.
return $date;
}
}
if ( ! function_exists( 'get_post_datetime' ) ) {
/**
* Retrieves post published or modified time as a DateTimeImmutable object instance.
*
* The object will be set to the timezone from WordPress settings.
*
* @since 5.3.0
*
* @param int|WP_Post|null $post Optional. Post ID or post object. Default is global $post object.
* @param string $field Optional. Published or modified time to use from database.
* Accepts 'date' or 'modified'. Default 'date'.
* @param string $source Optional. Local or UTC time to use from database.
* Accepts 'local' or 'gmt'. Default 'local'.
* @return DateTimeImmutable|false Time object on success, false on failure.
*/
function get_post_datetime( $post = null, $field = 'date', $source = 'local' ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Verbatim WordPress core polyfill.
$post = get_post( $post );
if ( ! $post ) {
return false;
}
$wp_timezone = wp_timezone();
if ( 'gmt' === $source ) {
$time = ( 'modified' === $field ) ? $post->post_modified_gmt : $post->post_date_gmt;
$timezone = new DateTimeZone( 'UTC' );
} else {
$time = ( 'modified' === $field ) ? $post->post_modified : $post->post_date;
$timezone = $wp_timezone;
}
if ( empty( $time ) || '0000-00-00 00:00:00' === $time ) {
return false;
}
$datetime = date_create_immutable_from_format( 'Y-m-d H:i:s', $time, $timezone );
if ( false === $datetime ) {
return false;
}
return $datetime->setTimezone( $wp_timezone );
}
}
if ( ! function_exists( 'get_post_timestamp' ) ) {
/**
* Retrieves post published or modified time as a Unix timestamp.
*
* @since 5.3.0
*
* @param int|WP_Post|null $post Optional. Post ID or post object. Default is global $post object.
* @param string $field Optional. Published or modified time to use from database.
* Accepts 'date' or 'modified'. Default 'date'.
* @return int|false Unix timestamp on success, false on failure.
*/
function get_post_timestamp( $post = null, $field = 'date' ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Verbatim WordPress core polyfill.
$datetime = get_post_datetime( $post, $field );
if ( false === $datetime ) {
return false;
}
return $datetime->getTimestamp();
}
}

View file

@ -224,7 +224,7 @@ function wpvulnerability_core_get_installed() {
// Return the JSON-encoded array of core vulnerabilities. // Return the JSON-encoded array of core vulnerabilities.
$encoded = wp_json_encode( $core ); $encoded = wp_json_encode( $core );
return false !== $encoded ? $encoded : ''; return false !== $encoded ? $encoded : '[]';
} }
/** /**

View file

@ -377,10 +377,59 @@ function wpvulnerability_debug_get_component_status() {
return $status; return $status;
} }
/**
* Returns the slug of the first installed plugin or theme.
*
* Mirrors the slug derivation used by the vulnerability data loaders: the
* folder name, falling back to the text domain for single-file plugins.
*
* @since 5.1.6
*
* @param string $type Either 'plugin' or 'theme'.
*
* @return string Slug, or an empty string when nothing of that type is installed.
*/
function wpvulnerability_debug_first_installed_slug( $type ) {
if ( 'theme' === $type ) {
foreach ( wp_get_themes() as $theme ) {
return $theme->get_stylesheet();
}
return '';
}
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( ! function_exists( 'get_plugins' ) ) {
return '';
}
foreach ( get_plugins() as $plugin_file => $plugin_data ) {
$folder = explode( '/', $plugin_file );
$slug = trim( (string) $folder[0] );
if ( '' !== $slug && false === strpos( $plugin_file, '/' ) ) {
// Single-file plugin at the plugins root: use the file name.
$slug = basename( $slug, '.php' );
}
if ( '' === $slug && isset( $plugin_data['TextDomain'] ) ) {
$td_raw = $plugin_data['TextDomain'];
$slug = trim( is_scalar( $td_raw ) ? (string) $td_raw : '' );
}
if ( '' !== $slug ) {
return $slug;
}
}
return '';
}
/** /**
* Tests API connectivity for a specific component. * Tests API connectivity for a specific component.
* *
* @since 4.3.0 * @since 4.3.0
* @since 5.1.6 Core collapses pre-release versions to their stable milestone,
* and plugins/themes use the real slug-based API routes.
* *
* @param string $component The component to test (e.g., 'core', 'plugins', 'php'). * @param string $component The component to test (e.g., 'core', 'plugins', 'php').
* *
@ -403,33 +452,45 @@ function wpvulnerability_debug_test_api_component( $component ) {
return $result; return $result;
} }
// Get version for the component. // Build the API URL for the component. Core and software routes take a
// version; the plugin/theme routes take a slug only.
$url = '';
$version = null; $version = null;
switch ( $component ) { switch ( $component ) {
case 'core': case 'core':
global $wp_version; global $wp_version;
$version = $wp_version; // The API only serves stable milestones: collapse pre-release
// suffixes (e.g. 7.1-alpha-62421) so the check keeps working.
$version = preg_replace( '/-(?:alpha|beta|rc).*$/i', '', trim( (string) $wp_version ) );
if ( ! is_string( $version ) || '' === $version ) {
$result['message'] = __( 'WordPress version not detected for this component.', 'wpvulnerability' );
return $result;
}
$url = WPVULNERABILITY_API_HOST . 'core/' . wpvulnerability_sanitize_version( $version ) . '/';
break; break;
case 'plugins': case 'plugins':
case 'themes': case 'themes':
// Use a generic version for testing. $slug = wpvulnerability_debug_first_installed_slug( 'plugins' === $component ? 'plugin' : 'theme' );
$version = '1.0.0'; if ( '' === $slug ) {
$result['message'] = 'plugins' === $component
? __( 'No plugins detected to test the API endpoint.', 'wpvulnerability' )
: __( 'No themes detected to test the API endpoint.', 'wpvulnerability' );
return $result;
}
$url = WPVULNERABILITY_API_HOST . ( 'plugins' === $component ? 'plugin/' : 'theme/' ) . sanitize_title( $slug ) . '/';
break; break;
default: default:
if ( function_exists( 'wpvulnerability_get_software_version' ) ) { if ( function_exists( 'wpvulnerability_get_software_version' ) ) {
$version = wpvulnerability_get_software_version( $component ); $version = wpvulnerability_get_software_version( $component );
} }
if ( ! $version ) {
$result['message'] = __( 'No local version detected; the API endpoint was not called.', 'wpvulnerability' );
return $result;
}
$url = WPVULNERABILITY_API_HOST . $component . '/' . wpvulnerability_sanitize_version( $version ) . '/';
break; break;
} }
if ( ! $version ) {
$result['message'] = __( 'Version not detected for this component.', 'wpvulnerability' );
return $result;
}
// Build API URL.
$url = WPVULNERABILITY_API_HOST . $component . '/' . $version . '/';
// Execute request with timing. // Execute request with timing.
$start_time = microtime( true ); $start_time = microtime( true );
$response = wp_remote_get( $response = wp_remote_get(
@ -494,7 +555,7 @@ function wpvulnerability_debug_get_cron_status() {
'scheduled' => false, 'scheduled' => false,
), ),
'send_notification' => array( 'send_notification' => array(
'hook' => 'wpvulnerability_send_notification', 'hook' => 'wpvulnerability_notification',
'next_run' => null, 'next_run' => null,
'last_run' => null, 'last_run' => null,
'scheduled' => false, 'scheduled' => false,
@ -509,34 +570,75 @@ function wpvulnerability_debug_get_cron_status() {
} }
// Check notification cron. // Check notification cron.
$next_notification = wp_next_scheduled( 'wpvulnerability_send_notification' ); $next_notification = wp_next_scheduled( 'wpvulnerability_notification' );
if ( $next_notification ) { if ( $next_notification ) {
$cron_status['send_notification']['next_run'] = $next_notification; $cron_status['send_notification']['next_run'] = $next_notification;
$cron_status['send_notification']['scheduled'] = true; $cron_status['send_notification']['scheduled'] = true;
} }
// Try to get last run times from logs. // Last run = most recent API response log (stored as the wpvulnerability_log CPT).
$logs_raw = is_multisite() ? get_site_option( 'wpvulnerability-logs', array() ) : get_option( 'wpvulnerability-logs', array() ); $last_log = get_posts(
$logs = is_array( $logs_raw ) ? $logs_raw : array(); array(
if ( ! empty( $logs ) ) { 'post_type' => 'wpvulnerability_log',
// Get the most recent log entry for each type. 'post_status' => 'any',
foreach ( array_reverse( $logs ) as $log ) { 'posts_per_page' => 1,
if ( ! is_array( $log ) ) { 'orderby' => 'date',
continue; } 'order' => 'DESC',
if ( isset( $log['time'] ) && isset( $log['url'] ) ) { 'fields' => 'ids',
$timestamp = $log['time']; 'no_found_rows' => true,
)
// Check if this is an update-related log. );
if ( ! $cron_status['update_database']['last_run'] ) { if ( ! empty( $last_log ) ) {
$cron_status['update_database']['last_run'] = $timestamp; $timestamp = get_post_timestamp( $last_log[0] );
} if ( false !== $timestamp ) {
} $cron_status['update_database']['last_run'] = $timestamp;
} }
} }
return $cron_status; return $cron_status;
} }
/**
* Masks sensitive configuration values (webhook URLs, tokens, emails).
*
* Used whenever configuration data is displayed or exported for debugging,
* so shared debug files never contain usable secrets or recipient addresses.
*
* @since 5.1.3
*
* @param mixed $config Plugin configuration.
* @return array<mixed, mixed> Configuration with sensitive values masked.
*/
function wpvulnerability_debug_mask_config( $config ) {
if ( ! is_array( $config ) ) {
return array();
}
$secret_keys = array( 'slack_webhook', 'teams_webhook', 'discord_webhook', 'telegram_bot_token', 'telegram_chat_id' );
foreach ( $secret_keys as $key ) {
if ( isset( $config[ $key ] ) && is_scalar( $config[ $key ] ) && '' !== (string) $config[ $key ] ) {
$value = (string) $config[ $key ];
$config[ $key ] = strlen( $value ) > 8 ? substr( $value, 0, 4 ) . '...' . substr( $value, -4 ) : '...';
}
}
if ( isset( $config['emails'] ) && is_scalar( $config['emails'] ) && '' !== (string) $config['emails'] ) {
$masked = array();
foreach ( explode( ',', (string) $config['emails'] ) as $email ) {
$parts = explode( '@', trim( $email ) );
if ( count( $parts ) < 2 || '' === $parts[1] ) {
$masked[] = '...';
continue;
}
$masked[] = ( '' !== $parts[0] ? substr( $parts[0], 0, 1 ) . '***' : '***' ) . '@' . $parts[1];
}
$config['emails'] = implode( ',', $masked );
}
return $config;
}
/** /**
* Exports comprehensive debug information as JSON. * Exports comprehensive debug information as JSON.
* *
@ -553,7 +655,7 @@ function wpvulnerability_debug_export_info() {
'timestamp' => current_time( 'mysql' ), 'timestamp' => current_time( 'mysql' ),
'system_info' => wpvulnerability_debug_get_system_info(), 'system_info' => wpvulnerability_debug_get_system_info(),
'components' => wpvulnerability_debug_get_component_status(), 'components' => wpvulnerability_debug_get_component_status(),
'configuration' => $config, 'configuration' => wpvulnerability_debug_mask_config( $config ),
'cron_status' => wpvulnerability_debug_get_cron_status(), 'cron_status' => wpvulnerability_debug_get_cron_status(),
'vulnerability_counts' => array(), 'vulnerability_counts' => array(),
); );
@ -594,6 +696,13 @@ function wpvulnerability_debug_clear_all_caches() {
} }
} }
// Plugin data timestamp option, written by wpvulnerability_plugin_get_data().
if ( is_multisite() ) {
delete_site_option( 'wpvulnerability-plugins-cache-data' );
} else {
delete_option( 'wpvulnerability-plugins-cache-data' );
}
return true; return true;
} }
@ -657,8 +766,318 @@ function wpvulnerability_debug_get_option_names() {
*/ */
function wpvulnerability_debug_get_option_value( $option_name ) { function wpvulnerability_debug_get_option_value( $option_name ) {
if ( is_multisite() ) { if ( is_multisite() ) {
return get_site_option( $option_name, null ); $value = get_site_option( $option_name, null );
} else { } else {
return get_option( $option_name, null ); $value = get_option( $option_name, null );
} }
// Mask secrets when displaying the configuration in the options viewer.
if ( 'wpvulnerability-config' === $option_name && is_array( $value ) ) {
$value = wpvulnerability_debug_mask_config( $value );
}
return $value;
}
/**
* Returns the PHP extensions WordPress makes use of, grouped by relevance.
*
* Purely informational: presence or absence of an extension is neither good
* nor bad. Grouping and notes follow the WordPress core recommendations.
*
* @since 5.1.6
*
* @return array<array{label: string, extensions: array<string, string>}> Each group has a
* label and a map of extension name to description.
*/
function wpvulnerability_debug_get_php_extensions() {
return array(
array(
'label' => __( 'Built-in (no hosting action required)', 'wpvulnerability' ),
'extensions' => array(
'pcre' => __( 'Regular expression engine used throughout PHP and WordPress.', 'wpvulnerability' ),
),
),
array(
'label' => __( 'Required', 'wpvulnerability' ),
'extensions' => array(
'hash' => __( 'Hashing, including passwords and update packages.', 'wpvulnerability' ),
'json' => __( 'Communications with other servers and JSON data processing.', 'wpvulnerability' ),
'mysqli' => __( 'Connects to MySQL/MariaDB for database interactions.', 'wpvulnerability' ),
),
),
array(
'label' => __( 'Highly recommended', 'wpvulnerability' ),
'extensions' => array(
'curl' => __( 'Performs remote request operations.', 'wpvulnerability' ),
'dom' => __( 'Validates Text Widget content and configures IIS7+ automatically.', 'wpvulnerability' ),
'exif' => __( 'Works with metadata stored in images.', 'wpvulnerability' ),
'fileinfo' => __( 'Detects MIME types of file uploads.', 'wpvulnerability' ),
'igbinary' => __( 'Drop-in replacement for the standard PHP serializer; improves performance.', 'wpvulnerability' ),
'imagick' => __( 'Better image quality for media uploads and PDF thumbnail support.', 'wpvulnerability' ),
'intl' => __( 'Locale-aware operations: formatting, transliteration, collation, timezones.', 'wpvulnerability' ),
'mbstring' => __( 'Properly handles UTF-8 text.', 'wpvulnerability' ),
'openssl' => __( 'SSL-based connections to other hosts.', 'wpvulnerability' ),
'xml' => __( 'XML parsing, such as from a third-party site.', 'wpvulnerability' ),
'zip' => __( 'Decompresses Plugin, Theme, and WordPress update packages.', 'wpvulnerability' ),
),
),
array(
'label' => __( 'Object cache (only one is needed)', 'wpvulnerability' ),
'extensions' => array(
'apcu' => __( 'In-memory key-value store for PHP.', 'wpvulnerability' ),
'memcached' => __( 'Distributed memory object caching system.', 'wpvulnerability' ),
'opcache' => __( 'Stores precompiled PHP bytecode to reduce repeated parsing and compilation.', 'wpvulnerability' ),
'redis' => __( 'PHP extension for interfacing with Redis.', 'wpvulnerability' ),
),
),
array(
'label' => __( 'Optional improvements', 'wpvulnerability' ),
'extensions' => array(
'timezonedb' => __( 'Timezone database used by the PHP date and time functions.', 'wpvulnerability' ),
),
),
array(
'label' => __( 'Fallbacks / situational use', 'wpvulnerability' ),
'extensions' => array(
'bcmath' => __( 'Arbitrary precision mathematics.', 'wpvulnerability' ),
'filter' => __( 'Securely filters user input.', 'wpvulnerability' ),
'gd' => __( 'Functionally limited fallback for image manipulation when Imagick is unavailable.', 'wpvulnerability' ),
'iconv' => __( 'Converts between character sets.', 'wpvulnerability' ),
'shmop' => __( 'Reads, writes, creates and deletes Unix shared memory segments.', 'wpvulnerability' ),
'simplexml' => __( 'XML parsing.', 'wpvulnerability' ),
'sodium' => __( 'Signature validation and securely random bytes.', 'wpvulnerability' ),
'xmlreader' => __( 'XML parsing.', 'wpvulnerability' ),
'zlib' => __( 'Gzip compression and decompression.', 'wpvulnerability' ),
),
),
array(
'label' => __( 'File changes when files are not writeable (transports)', 'wpvulnerability' ),
'extensions' => array(
'ssh2' => __( 'Shell, remote execution, tunneling and file transfer over SSH.', 'wpvulnerability' ),
'ftp' => __( 'FTP client access for updates and installations.', 'wpvulnerability' ),
'sockets' => __( 'Low-level socket communication functions.', 'wpvulnerability' ),
),
),
);
}
/**
* Detects whether a PHP extension is loaded.
*
* Handles special cases where the internal extension name differs from the
* common name or where availability depends on functions instead.
*
* @since 5.1.6
*
* @param string $extension Extension name as listed in the recommendations.
*
* @return bool True when the extension is loaded.
*/
function wpvulnerability_debug_extension_loaded( $extension ) {
if ( 'opcache' === $extension ) {
return extension_loaded( 'opcache' )
|| extension_loaded( 'Zend OPcache' )
|| function_exists( 'opcache_get_status' );
}
return extension_loaded( $extension );
}
/**
* Gathers system package information relevant to WordPress.
*
* Informational only. Shell probes honour the plugin shell-exec policy
* (security mode, disable constant) through the safe wrapper; when probing
* is not possible the version is reported as unknown.
*
* @since 5.1.6
*
* @return array<array{name: string, available: bool, version: string|null}> Package rows.
*/
function wpvulnerability_debug_get_system_packages() {
$packages = array();
// cURL: reuse the shared detector (PHP extension first, then CLI probe).
$curl_version = function_exists( 'wpvulnerability_detect_curl' ) ? wpvulnerability_detect_curl() : null;
$packages[] = array(
'name' => 'curl',
'available' => null !== $curl_version,
'version' => $curl_version,
);
// ImageMagick: reuse the shared detector.
$imagemagick = function_exists( 'wpvulnerability_detect_imagemagick' ) ? wpvulnerability_detect_imagemagick() : array();
$im_raw = isset( $imagemagick['version'] ) ? $imagemagick['version'] : null;
$im_version = is_string( $im_raw ) ? $im_raw : null;
$packages[] = array(
'name' => 'ImageMagick',
'available' => null !== $im_version,
'version' => $im_version,
);
// Ghost Script: enables Imagick/ImageMagick PDF thumbnail generation.
$gs_version = null;
if ( wpvulnerability_can_shell_exec() ) {
$gs_output = wpvulnerability_safe_shell_exec( 'gs', 'gs --version' );
$gs_matches = array();
if ( is_string( $gs_output ) && preg_match( '/(\d+\.\d+(?:\.\d+)?)/', trim( $gs_output ), $gs_matches ) ) {
$gs_version = $gs_matches[1];
}
}
$packages[] = array(
'name' => 'Ghost Script',
'available' => null !== $gs_version,
'version' => $gs_version,
);
// OpenSSL: report the OpenSSL linked into the PHP build.
$openssl_version = null;
if ( defined( 'OPENSSL_VERSION_TEXT' ) && preg_match( '/(?:OpenSSL|LibreSSL|BoringSSL)\s+(\d[\w.]*)/', (string) OPENSSL_VERSION_TEXT, $o_matches ) ) {
$openssl_version = $o_matches[1];
}
$packages[] = array(
'name' => 'OpenSSL',
'available' => defined( 'OPENSSL_VERSION_TEXT' ),
'version' => $openssl_version,
);
// WebP and AVIF support: check Imagick formats, GD, then CLI tools.
foreach (
array(
'WebP' => array(
'format' => 'WEBP',
'gd_key' => 'WebP Support',
'tool' => 'cwebp',
'command' => 'cwebp -version',
),
'AVIF' => array(
'format' => 'AVIF',
'gd_key' => 'AVIF Support',
'tool' => 'avifenc',
'command' => 'avifenc --version',
),
)
as $format_name => $format_check
) {
$supported = false;
$version = null;
if ( extension_loaded( 'imagick' ) && class_exists( 'Imagick' ) ) {
try {
$imagick = new Imagick();
$supported = in_array( strtoupper( $format_check['format'] ), $imagick->queryFormats( $format_check['format'] ), true );
} catch ( Exception $e ) {
$supported = false;
}
}
if ( ! $supported && extension_loaded( 'gd' ) && function_exists( 'gd_info' ) ) {
$gd_info = gd_info();
$supported = ! empty( $gd_info[ $format_check['gd_key'] ] );
}
if ( ! $supported && wpvulnerability_can_shell_exec() ) {
$tool_output = wpvulnerability_safe_shell_exec( $format_check['tool'], $format_check['command'] );
if ( is_string( $tool_output ) && '' !== trim( $tool_output ) ) {
$supported = true;
$v_matches = array();
if ( preg_match( '/(\d+\.\d+(?:\.\d+)?)/', $tool_output, $v_matches ) ) {
$version = $v_matches[1];
}
}
}
$packages[] = array(
'name' => $format_name,
'available' => $supported,
'version' => $version,
);
}
return $packages;
}
/**
* Renders the Debug tab section: PHP extensions and system packages.
*
* Informational listing only: nothing here is good or bad.
*
* @since 5.1.6
*
* @return void
*/
function wpvulnerability_render_debug_section_php_extensions() {
$groups = wpvulnerability_debug_get_php_extensions();
$packages = wpvulnerability_debug_get_system_packages();
?>
<div class="wpvulnerability-debug-section">
<h3><?php esc_html_e( 'PHP Extensions', 'wpvulnerability' ); ?></h3>
<p>
<?php esc_html_e( 'WordPress core makes use of various PHP extensions when they are available. This list is informational only: a missing extension is neither good nor bad.', 'wpvulnerability' ); ?>
</p>
<?php
foreach ( $groups as $group ) :
?>
<h4><?php echo esc_html( $group['label'] ); ?></h4>
<table class="widefat striped" style="max-width: 720px;">
<tbody>
<?php
foreach ( $group['extensions'] as $extension => $description ) :
$loaded = wpvulnerability_debug_extension_loaded( $extension );
?>
<tr>
<td style="width: 90px;">
<span style="color: <?php echo $loaded ? '#00a32a;' : '#8c8f94;'; ?>"><?php echo $loaded ? esc_html__( 'Loaded', 'wpvulnerability' ) : esc_html__( 'Not loaded', 'wpvulnerability' ); ?></span>
</td>
<td style="width: 110px;"><code><?php echo esc_html( $extension ); ?></code></td>
<td><?php echo esc_html( $description ); ?></td>
</tr>
<?php
endforeach;
?>
</tbody>
</table>
<?php
endforeach;
?>
<h3><?php esc_html_e( 'System Packages', 'wpvulnerability' ); ?></h3>
<p>
<?php esc_html_e( 'System software WordPress can leverage. Informational only.', 'wpvulnerability' ); ?>
</p>
<table class="widefat striped" style="max-width: 720px;">
<tbody>
<?php
foreach ( $packages as $package ) :
?>
<tr>
<td style="width: 90px;">
<span style="color: <?php echo $package['available'] ? '#00a32a;' : '#8c8f94;'; ?>">
<?php echo $package['available'] ? esc_html__( 'Available', 'wpvulnerability' ) : esc_html__( 'Not detected', 'wpvulnerability' ); ?>
</span>
</td>
<td style="width: 110px;"><code><?php echo esc_html( (string) $package['name'] ); ?></code></td>
<td>
<?php
if ( null !== $package['version'] && '' !== (string) $package['version'] ) {
echo esc_html(
sprintf(
/* translators: %s: package version number */
__( 'Version %s', 'wpvulnerability' ),
(string) $package['version']
)
);
} elseif ( ! $package['available'] ) {
esc_html_e( 'No local version detected.', 'wpvulnerability' );
}
?>
</td>
</tr>
<?php
endforeach;
?>
</tbody>
</table>
</div>
<?php
} }

View file

@ -90,12 +90,16 @@ function wpvulnerability_capabilities() {
* *
* @since 3.4.0 * @since 3.4.0
* @since 4.3.0 Enhanced with 4-level security checks and component-specific control. * @since 4.3.0 Enhanced with 4-level security checks and component-specific control.
* @since 5.1.4 The live probe result is now cached per request.
* *
* @param string $component Optional. Component name for granular control. * @param string $component Optional. Component name for granular control.
* *
* @return bool True if `shell_exec` is available and allowed, false otherwise. * @return bool True if `shell_exec` is available and allowed, false otherwise.
*/ */
function wpvulnerability_can_shell_exec( $component = '' ) { function wpvulnerability_can_shell_exec( $component = '' ) {
static $probe_result = null;
// Level 1: Global disable via constant. // Level 1: Global disable via constant.
if ( defined( 'WPVULNERABILITY_DISABLE_SHELL_EXEC' ) && WPVULNERABILITY_DISABLE_SHELL_EXEC ) { if ( defined( 'WPVULNERABILITY_DISABLE_SHELL_EXEC' ) && WPVULNERABILITY_DISABLE_SHELL_EXEC ) {
return false; return false;
@ -136,11 +140,15 @@ function wpvulnerability_can_shell_exec( $component = '' ) {
return false; return false;
} }
// Try to execute a simple command to confirm functionality. // Try to execute a simple command to confirm functionality. The probe
$test = @shell_exec( escapeshellcmd( 'echo test' ) ); // phpcs:ignore // result is cached per request: PHP configuration cannot change mid-request
// and this function runs several times per admin/cron cycle.
if ( null === $probe_result ) {
$probe_result = @shell_exec( escapeshellcmd( 'echo test' ) ); // phpcs:ignore
}
// If the command execution failed or returned null, shell_exec is not working. // If the command execution failed or returned null, shell_exec is not working.
return null !== $test; return null !== $probe_result;
} }
/** /**
@ -652,31 +660,45 @@ add_action( 'wpvulnerability_cleanup_logs', 'wpvulnerability_delete_expired_logs
* @return void * @return void
*/ */
function wpvulnerability_delete_all_logs() { function wpvulnerability_delete_all_logs() {
do {
$logs = get_posts(
array(
'post_type' => 'wpvulnerability_log',
'post_status' => 'publish',
'fields' => 'ids',
'posts_per_page' => 100,
'orderby' => 'date',
'order' => 'ASC',
'no_found_rows' => true,
'cache_results' => false,
'update_post_term_cache' => false,
'update_post_meta_cache' => false,
)
);
if ( empty( $logs ) ) { // The "delete logs" action runs at file scope during init, potentially
break; // before this file's init callbacks register the post types: queries for
} // unregistered post types silently return nothing.
if ( ! post_type_exists( 'wpvulnerability_log' ) ) {
wpvulnerability_register_log_post_type();
}
if ( ! post_type_exists( 'wpv_shell_log' ) ) {
wpvulnerability_register_shell_log_post_type();
}
foreach ( $logs as $log_id ) { // Purge both log stores: API request logs and shell audit logs.
wp_delete_post( $log_id, true ); foreach ( array( 'wpvulnerability_log', 'wpv_shell_log' ) as $log_post_type ) {
} do {
$logs_count = count( $logs ); $logs = get_posts(
} while ( $logs_count >= 100 ); array(
'post_type' => $log_post_type,
'post_status' => 'any',
'fields' => 'ids',
'posts_per_page' => 100,
'orderby' => 'date',
'order' => 'ASC',
'no_found_rows' => true,
'cache_results' => false,
'update_post_term_cache' => false,
'update_post_meta_cache' => false,
)
);
if ( empty( $logs ) ) {
break;
}
foreach ( $logs as $log_id ) {
wp_delete_post( $log_id, true );
}
$logs_count = count( $logs );
} while ( $logs_count >= 100 );
}
} }
/** /**
@ -735,7 +757,11 @@ function wpvulnerability_normalize_notify_settings( $notify ) {
if ( is_array( $notify ) ) { if ( is_array( $notify ) ) {
foreach ( $notify as $channel => $value ) { foreach ( $notify as $channel => $value ) {
$normalized[ $channel ] = wpvulnerability_normalize_yes_no( $value ); // Only the five known channels are stored; anything else from
// a crafted request is discarded.
if ( isset( $defaults[ (string) $channel ] ) ) {
$normalized[ (string) $channel ] = wpvulnerability_normalize_yes_no( $value );
}
} }
} }
@ -1070,10 +1096,6 @@ function wpvulnerability_detect_redis() {
$result['method'] = 'redis_extension'; $result['method'] = 'redis_extension';
$result['reliability'] = 90; $result['reliability'] = 90;
if ( $temporary_connection ) {
$redis_client->close();
}
return $result; return $result;
} }
} }
@ -1909,14 +1931,28 @@ function wpvulnerability_get( $type, $slug = '', $cache = 1 ) {
wp_die( 'Unknown vulnerability type sent.' ); wp_die( 'Unknown vulnerability type sent.' );
} }
// Validate slug for plugin or theme. // Validate and normalize slug for plugin or theme.
if ( ( 'plugin' === $type || 'theme' === $type ) && empty( sanitize_title( $slug ) ) ) { if ( 'plugin' === $type || 'theme' === $type ) {
return false; $slug = sanitize_title( (string) $slug );
if ( '' === $slug ) {
return false;
}
} }
// Validate slug for core. // Validate and normalize slug for core.
if ( 'core' === $type && ! wpvulnerability_sanitize_version( $slug ) ) { if ( 'core' === $type ) {
return false; $slug = wpvulnerability_sanitize_version( (string) $slug );
if ( ! $slug ) {
return false;
}
// The API only serves stable milestones: collapse pre-release suffixes
// (e.g. 7.1-alpha-62421 or 6.9-beta1) to the milestone (7.1 / 6.9) so
// core lookups keep working on development installations.
$milestone = preg_replace( '/-(?:alpha|beta|rc).*$/i', '', $slug );
if ( is_string( $milestone ) && '' !== $milestone ) {
$slug = $milestone;
}
} }
// Cache key. // Cache key.
@ -1931,14 +1967,16 @@ function wpvulnerability_get( $type, $slug = '', $cache = 1 ) {
$response = wp_remote_get( $url, array( 'timeout' => 2.5 ) ); $response = wp_remote_get( $url, array( 'timeout' => 2.5 ) );
wpvulnerability_maybe_log_api_response( $url, $response ); wpvulnerability_maybe_log_api_response( $url, $response );
if ( ! is_wp_error( $response ) ) { if ( ! is_wp_error( $response ) && 200 === (int) wp_remote_retrieve_response_code( $response ) ) {
$body = wp_remote_retrieve_body( $response ); $body = wp_remote_retrieve_body( $response );
// Cache the response data. // Cache only valid JSON responses so error pages never poison the cache.
if ( is_multisite() ) { if ( is_array( json_decode( $body, true ) ) && $cache ) {
set_site_transient( $key, $body, HOUR_IN_SECONDS * wpvulnerability_cache_hours() ); if ( is_multisite() ) {
} else { set_site_transient( $key, $body, HOUR_IN_SECONDS * wpvulnerability_cache_hours() );
set_transient( $key, $body, HOUR_IN_SECONDS * wpvulnerability_cache_hours() ); } else {
set_transient( $key, $body, HOUR_IN_SECONDS * wpvulnerability_cache_hours() );
}
} }
$vulnerability_data = $body; // Use the fresh data. $vulnerability_data = $body; // Use the fresh data.
@ -2252,15 +2290,18 @@ function wpvulnerability_get_statistics( $cache = 1 ) {
$response = wp_remote_get( $url, array( 'timeout' => 2.5 ) ); $response = wp_remote_get( $url, array( 'timeout' => 2.5 ) );
wpvulnerability_maybe_log_api_response( $url, $response ); wpvulnerability_maybe_log_api_response( $url, $response );
if ( ! is_wp_error( $response ) ) { if ( ! is_wp_error( $response ) && 200 === (int) wp_remote_retrieve_response_code( $response ) ) {
$body = wp_remote_retrieve_body( $response ); $body = wp_remote_retrieve_body( $response );
// Cache the response data.
if ( is_multisite() ) { // Cache only valid JSON responses so error pages never poison the cache.
set_site_transient( $key, $body, HOUR_IN_SECONDS * wpvulnerability_cache_hours() ); if ( is_array( json_decode( $body, true ) ) ) {
} else { if ( is_multisite() ) {
set_transient( $key, $body, HOUR_IN_SECONDS * wpvulnerability_cache_hours() ); set_site_transient( $key, $body, HOUR_IN_SECONDS * wpvulnerability_cache_hours() );
} else {
set_transient( $key, $body, HOUR_IN_SECONDS * wpvulnerability_cache_hours() );
}
$vulnerability = $body; // Use the fresh data.
} }
$vulnerability = $body; // Use the fresh data.
} }
} }
@ -2437,6 +2478,16 @@ function wpvulnerability_statistics_get() {
* @return list<array<string, mixed>>|false Returns an array of vulnerabilities, or false if there are none. * @return list<array<string, mixed>>|false Returns an array of vulnerabilities, or false if there are none.
*/ */
function wpvulnerability_get_vulnerabilities( $type, $version, $cache = 1 ) { function wpvulnerability_get_vulnerabilities( $type, $version, $cache = 1 ) {
// Validate vulnerability type and version before building the request URL.
$type = strtolower( trim( (string) $type ) );
$version = wpvulnerability_sanitize_version( (string) $version );
$valid_types = array( 'php', 'apache', 'nginx', 'mariadb', 'mysql', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
if ( ! in_array( $type, $valid_types, true ) || ! $version ) {
return false;
}
$key = 'wpvulnerability_' . $type; $key = 'wpvulnerability_' . $type;
$vulnerability_data = null; $vulnerability_data = null;
$vulnerability = array(); $vulnerability = array();
@ -2452,9 +2503,11 @@ function wpvulnerability_get_vulnerabilities( $type, $version, $cache = 1 ) {
$response = wp_remote_get( $url, array( 'timeout' => 2.5 ) ); $response = wp_remote_get( $url, array( 'timeout' => 2.5 ) );
wpvulnerability_maybe_log_api_response( $url, $response ); wpvulnerability_maybe_log_api_response( $url, $response );
if ( ! is_wp_error( $response ) ) { if ( ! is_wp_error( $response ) && 200 === (int) wp_remote_retrieve_response_code( $response ) ) {
$body = wp_remote_retrieve_body( $response ); $body = wp_remote_retrieve_body( $response );
if ( $cache ) {
// Cache only valid JSON responses so error pages never poison the cache.
if ( is_array( json_decode( $body, true ) ) && $cache ) {
if ( is_multisite() ) { if ( is_multisite() ) {
set_site_transient( $key, $body, HOUR_IN_SECONDS * wpvulnerability_cache_hours() ); set_site_transient( $key, $body, HOUR_IN_SECONDS * wpvulnerability_cache_hours() );
} else { } else {
@ -2616,6 +2669,9 @@ function wpvulnerability_validate_shell_command( $command ) {
'caddy', 'caddy',
'php', 'php',
'curl', 'curl',
'gs',
'cwebp',
'avifenc',
); );
// Extract the base command (first word). // Extract the base command (first word).

View file

@ -9,60 +9,6 @@
defined( 'ABSPATH' ) || die( 'No script kiddies please!' ); defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
/**
* Adds a custom schedule for a weekly cron job.
*
* This function adds a new schedule interval of one week (604800 seconds)
* to the system's available cron schedules. It allows tasks to be scheduled
* to run every week using the 'weekly' interval.
*
* @since 2.0.0
*
* @param array<string, array<string, int|string>> $schedules The existing system schedules.
*
* @return array<string, array<string, int|string>> The updated list of schedules with the added weekly interval.
*/
function wpvulnerability_add_every_week( $schedules ) {
// Add a weekly schedule interval of 604800 seconds (1 week).
// Use did_action('init') guard: cron_schedules fires before init (WP 6.7+ textdomain notice).
$schedules['weekly'] = array(
'interval' => 604800,
'display' => did_action( 'init' ) ? __( 'Every week', 'wpvulnerability' ) : 'Every week',
);
// Return the modified list of schedules.
return $schedules;
}
// Hook the function to the 'cron_schedules' filter to add the custom schedule.
add_filter( 'cron_schedules', 'wpvulnerability_add_every_week' );
/**
* Adds a custom schedule for daily events.
*
* This function adds a new schedule interval of one day (86400 seconds)
* to the system's available cron schedules. It allows tasks to be scheduled
* to run every day using the 'daily' interval.
*
* @since 2.0.0
*
* @param array<string, array<string, int|string>> $schedules List of available schedules.
*
* @return array<string, array<string, int|string>> Modified list of available schedules with the added daily interval.
*/
function wpvulnerability_add_every_day( $schedules ) {
// Define a new schedule with a 24 hour interval.
// Use did_action('init') guard: cron_schedules fires before init (WP 6.7+ textdomain notice).
$schedules['daily'] = array(
'interval' => 86400,
'display' => did_action( 'init' ) ? __( 'Every day', 'wpvulnerability' ) : 'Every day',
);
// Return the modified list of schedules.
return $schedules;
}
// Hook the function to the 'cron_schedules' filter to add the custom schedule.
add_filter( 'cron_schedules', 'wpvulnerability_add_every_day' );
/** /**
* Disables email notifications when requested via URL. * Disables email notifications when requested via URL.
* *
@ -174,6 +120,28 @@ function wpvulnerability_validate_webhook_url( $webhook_url, $allowed_hosts ) {
return $webhook_url; return $webhook_url;
} }
/**
* Masks a secret value for display in the admin UI.
*
* Per the project security checklist, secrets (webhook URLs, bot tokens) are
* never rendered in full: only their last 4 characters are shown.
*
* @since 5.1.4
*
* @param string $value Secret value to mask.
*
* @return string Masked value, or an empty string when the input is empty.
*/
function wpvulnerability_mask_secret( $value ) {
$value = (string) $value;
if ( '' === $value ) {
return '';
}
return '••••••••' . mb_substr( $value, -4 );
}
/** /**
* Retrieves the unsubscribe URL for WPVulnerability email notifications. * Retrieves the unsubscribe URL for WPVulnerability email notifications.
* *
@ -683,7 +651,7 @@ function wpvulnerability_execute_notification( $forced = false ) {
// Get the admin email. // Get the admin email.
$admin_email = is_multisite() ? get_site_option( 'admin_email' ) : get_bloginfo( 'admin_email' ); $admin_email = is_multisite() ? get_site_option( 'admin_email' ) : get_bloginfo( 'admin_email' );
$from_email = $admin_email; $from_email = is_scalar( $admin_email ) ? sanitize_email( (string) $admin_email ) : '';
// Check if WPVULNERABILITY_MAIL is defined and valid, and use it if available. // Check if WPVULNERABILITY_MAIL is defined and valid, and use it if available.
if ( defined( 'WPVULNERABILITY_MAIL' ) ) { if ( defined( 'WPVULNERABILITY_MAIL' ) ) {
@ -705,7 +673,7 @@ function wpvulnerability_execute_notification( $forced = false ) {
// Prepare email headers. // Prepare email headers.
$email_headers = array(); $email_headers = array();
$email_headers[] = 'From: WPVulnerability <' . ( is_scalar( $from_email ) ? (string) $from_email : '' ) . '>'; $email_headers[] = 'From: WPVulnerability <' . $from_email . '>';
$email_headers[] = 'Content-Type: text/html; charset=UTF-8'; $email_headers[] = 'Content-Type: text/html; charset=UTF-8';
if ( $forced && ( empty( $wpvulnerability_settings['emails'] ) ) ) { if ( $forced && ( empty( $wpvulnerability_settings['emails'] ) ) ) {
@ -715,7 +683,7 @@ function wpvulnerability_execute_notification( $forced = false ) {
$wpmail = false; $wpmail = false;
if ( $email_enabled ) { if ( $email_enabled || $forced ) {
$mail_to = is_array( $wpvulnerability_settings['emails'] ) ? array_map( $mail_to = is_array( $wpvulnerability_settings['emails'] ) ? array_map(
static function ( $e ) { static function ( $e ) {
return is_scalar( $e ) ? (string) $e : ''; return is_scalar( $e ) ? (string) $e : '';
@ -728,23 +696,27 @@ function wpvulnerability_execute_notification( $forced = false ) {
$text_message_body = wpvulnerability_html_to_plain_text( $email_content ); $text_message_body = wpvulnerability_html_to_plain_text( $email_content );
$text_message = trim( (string) ( $email_subject . "\n\n" . $text_message_body ) ); $text_message = trim( (string) ( $email_subject . "\n\n" . $text_message_body ) );
if ( $slack_enabled ) { // Track whether any webhook channel delivered successfully, so a webhook-only
wpvulnerability_send_slack_notification( ( is_scalar( $wpvulnerability_settings['slack_webhook'] ) ? (string) $wpvulnerability_settings['slack_webhook'] : '' ), $text_message ); // notification (email disabled) is not reported as a failure.
$webhook_sent = false;
if ( $slack_enabled && wpvulnerability_send_slack_notification( ( is_scalar( $wpvulnerability_settings['slack_webhook'] ) ? (string) $wpvulnerability_settings['slack_webhook'] : '' ), $text_message ) ) {
$webhook_sent = true;
} }
if ( $teams_enabled ) { if ( $teams_enabled && wpvulnerability_send_teams_notification( ( is_scalar( $wpvulnerability_settings['teams_webhook'] ) ? (string) $wpvulnerability_settings['teams_webhook'] : '' ), $text_message ) ) {
wpvulnerability_send_teams_notification( ( is_scalar( $wpvulnerability_settings['teams_webhook'] ) ? (string) $wpvulnerability_settings['teams_webhook'] : '' ), $text_message ); $webhook_sent = true;
} }
if ( $discord_enabled ) { if ( $discord_enabled && wpvulnerability_send_discord_notification( ( is_scalar( $wpvulnerability_settings['discord_webhook'] ) ? (string) $wpvulnerability_settings['discord_webhook'] : '' ), $text_message ) ) {
wpvulnerability_send_discord_notification( ( is_scalar( $wpvulnerability_settings['discord_webhook'] ) ? (string) $wpvulnerability_settings['discord_webhook'] : '' ), $text_message ); $webhook_sent = true;
} }
if ( $telegram_enabled ) { if ( $telegram_enabled && wpvulnerability_send_telegram_notification( ( is_scalar( $wpvulnerability_settings['telegram_bot_token'] ) ? (string) $wpvulnerability_settings['telegram_bot_token'] : '' ), ( is_scalar( $wpvulnerability_settings['telegram_chat_id'] ) ? (string) $wpvulnerability_settings['telegram_chat_id'] : '' ), $text_message ) ) {
wpvulnerability_send_telegram_notification( ( is_scalar( $wpvulnerability_settings['telegram_bot_token'] ) ? (string) $wpvulnerability_settings['telegram_bot_token'] : '' ), ( is_scalar( $wpvulnerability_settings['telegram_chat_id'] ) ? (string) $wpvulnerability_settings['telegram_chat_id'] : '' ), $text_message ); $webhook_sent = true;
} }
return $wpmail; return $wpmail || $webhook_sent;
} }
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOM properties follow upstream naming. // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOM properties follow upstream naming.

View file

@ -511,6 +511,8 @@ function wpvulnerability_plugin_get_vulnerabilities_clean() {
*/ */
function wpvulnerability_plugin_show_lastupdated( $column_name, $plugin_file, $plugin_data ) { function wpvulnerability_plugin_show_lastupdated( $column_name, $plugin_file, $plugin_data ) {
static $plugins_data = null;
$now = time(); $now = time();
$year = strtotime( '-1 year', $now ); $year = strtotime( '-1 year', $now );
@ -533,11 +535,14 @@ function wpvulnerability_plugin_show_lastupdated( $column_name, $plugin_file, $p
if ( '' !== $plugin_slug ) { if ( '' !== $plugin_slug ) {
// Retrieve the vulnerabilities for all plugins from the options table and decode the JSON. // Retrieve the vulnerabilities for all plugins once per request; this
$raw_plugins_data = is_multisite() ? get_site_option( 'wpvulnerability-plugins-data', '' ) : get_option( 'wpvulnerability-plugins-data', '' ); // callback runs for every row of the plugins list table.
$plugins_data = json_decode( is_string( $raw_plugins_data ) ? $raw_plugins_data : '', true ); if ( null === $plugins_data ) {
if ( ! is_array( $plugins_data ) ) { $raw_plugins_data = is_multisite() ? get_site_option( 'wpvulnerability-plugins-data', '' ) : get_option( 'wpvulnerability-plugins-data', '' );
$plugins_data = array(); $plugins_data = json_decode( is_string( $raw_plugins_data ) ? $raw_plugins_data : '', true );
if ( ! is_array( $plugins_data ) ) {
$plugins_data = array();
}
} }
// Get the plugin data from the stored data. // Get the plugin data from the stored data.
@ -548,15 +553,11 @@ function wpvulnerability_plugin_show_lastupdated( $column_name, $plugin_file, $p
$pd_latest = is_scalar( $pd_latest_raw ) ? intval( $pd_latest_raw ) : 0; $pd_latest = is_scalar( $pd_latest_raw ) ? intval( $pd_latest_raw ) : 0;
if ( $pd_latest > 0 ) { if ( $pd_latest > 0 ) {
$timestamp = $pd_latest; $timestamp = $pd_latest;
$df_raw = get_option( 'date_format', 'Y-m-d' ); $df_raw = get_option( 'date_format', 'Y-m-d' );
$date_format = is_scalar( $df_raw ) ? (string) $df_raw : 'Y-m-d'; $date_format = is_scalar( $df_raw ) ? (string) $df_raw : 'Y-m-d';
if ( function_exists( 'wp_date' ) ) { $plugin_data_updated = (string) wp_date( $date_format, $timestamp );
$plugin_data_updated = (string) wp_date( $date_format, $timestamp ); $plugin_data_ago = human_time_diff( $timestamp );
} else {
$plugin_data_updated = gmdate( $date_format, $timestamp );
}
$plugin_data_ago = human_time_diff( $timestamp );
$warning_date = $pd_latest < $year; $warning_date = $pd_latest < $year;
$pd_closed_raw = $pd['closed'] ?? 0; $pd_closed_raw = $pd['closed'] ?? 0;
@ -587,7 +588,7 @@ function wpvulnerability_plugin_show_lastupdated( $column_name, $plugin_file, $p
* Adds a 'Last Updated' column to the plugins table list in the WordPress admin area. * Adds a 'Last Updated' column to the plugins table list in the WordPress admin area.
* *
* This function iterates over the existing columns in the plugins table and inserts a new column titled 'Last Updated' * This function iterates over the existing columns in the plugins table and inserts a new column titled 'Last Updated'
* just before the 'auto-updates' column if it exists. If the 'auto-updates' column is not found, the 'Last Updated' * just after the 'description' column if it exists. If the 'description' column is not found, the 'Last Updated'
* column is appended at the end. The function is typically hooked to the 'manage_plugins_columns' filter in WordPress * column is appended at the end. The function is typically hooked to the 'manage_plugins_columns' filter in WordPress
* to modify the columns of the plugins table. * to modify the columns of the plugins table.
* *

View file

@ -247,7 +247,17 @@ function wpvulnerability_component_icon_html( $type ) {
if ( ! isset( $icon_map[ $type ] ) ) { if ( ! isset( $icon_map[ $type ] ) ) {
return ''; return '';
} }
return '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL . 'assets/' . $icon_map[ $type ] ) . '" class="wpvuln-component-icon" alt="" />';
// Explicit dimensions: this HTML is also embedded where the plugin CSS is
// not loaded (notification emails, Site Health), and without width/height
// the SVG renders at its intrinsic 800x800 size.
$alt = sprintf(
/* translators: %s: component type (plugin, theme, core, php, ...) */
__( '%s icon', 'wpvulnerability' ),
(string) $type
);
return '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL . 'assets/' . $icon_map[ $type ] ) . '" class="wpvuln-component-icon" width="16" height="16" alt="' . esc_attr( $alt ) . '" />';
} }
/** /**

View file

@ -27,10 +27,10 @@ function wpvulnerability_add_settings_link( $links ) {
// Determine the correct settings link based on the environment. // Determine the correct settings link based on the environment.
if ( is_multisite() && is_network_admin() ) { if ( is_multisite() && is_network_admin() ) {
// Network admin settings link for multisite. // Network admin settings link for multisite.
$links[] = '<a href="' . network_admin_url( 'settings.php?page=wpvulnerability-options' ) . '">' . __( 'Network Settings', 'wpvulnerability' ) . '</a>'; $links[] = '<a href="' . esc_url( network_admin_url( 'settings.php?page=wpvulnerability-options' ) ) . '">' . esc_html__( 'Network Settings', 'wpvulnerability' ) . '</a>';
} elseif ( ! is_multisite() && is_admin() ) { } elseif ( ! is_multisite() && is_admin() ) {
// Standard settings link for single site. // Standard settings link for single site.
$links[] = '<a href="' . get_admin_url( null, 'options-general.php?page=wpvulnerability-options' ) . '">' . __( 'Settings', 'wpvulnerability' ) . '</a>'; $links[] = '<a href="' . esc_url( get_admin_url( null, 'options-general.php?page=wpvulnerability-options' ) ) . '">' . esc_html__( 'Settings', 'wpvulnerability' ) . '</a>';
} }
} }
return $links; return $links;
@ -271,7 +271,10 @@ function wpvulnerability_initialize_plugin_data( $is_real_activation = false ) {
// add_site_option does not support autoload control). // add_site_option does not support autoload control).
$no_autoload_keys = array( $no_autoload_keys = array(
'wpvulnerability-plugins', 'wpvulnerability-plugins',
'wpvulnerability-plugins-cache',
'wpvulnerability-plugins-vulnerable',
'wpvulnerability-plugins-data', 'wpvulnerability-plugins-data',
'wpvulnerability-plugins-data-cache',
'wpvulnerability-themes', 'wpvulnerability-themes',
'wpvulnerability-core', 'wpvulnerability-core',
'wpvulnerability-php', 'wpvulnerability-php',
@ -351,6 +354,10 @@ function wpvulnerability_initialize_plugin_data( $is_real_activation = false ) {
*/ */
function wpvulnerability_activation() { function wpvulnerability_activation() {
wpvulnerability_initialize_plugin_data( true ); wpvulnerability_initialize_plugin_data( true );
// init has already fired during the activation request, so the deferred
// scheduling in wpvulnerability-schedule.php cannot run here; schedule directly.
wpvulnerability_schedule_core_events();
} }
/** /**
@ -365,7 +372,6 @@ function wpvulnerability_deactivation() {
$options = array( $options = array(
'wpvulnerability_settings', 'wpvulnerability_settings',
'wpvulnerability-data', 'wpvulnerability-data',
'wpvulnerability-analyze',
'wpvulnerability-themes', 'wpvulnerability-themes',
'wpvulnerability-themes-cache', 'wpvulnerability-themes-cache',
'wpvulnerability-themes-vulnerable', 'wpvulnerability-themes-vulnerable',
@ -607,29 +613,46 @@ function wpvulnerability_uninstall() {
} }
} }
// Delete all stored log entries in batches to avoid timeouts. // Delete all stored log entries - both API request logs and shell audit
$log_query_args = array( // logs - in batches to avoid timeouts. The post types may not be
'post_type' => 'wpvulnerability_log', // registered yet when this routine runs early (the admin reset executes
'fields' => 'ids', // during init, and uninstall.php does not process init): queries for
'post_status' => 'any', // unregistered post types silently return nothing, so registration is
'posts_per_page' => 100, // forced here first.
'orderby' => 'ID', foreach (
'order' => 'ASC', array(
'no_found_rows' => true, 'wpvulnerability_log' => 'wpvulnerability_register_log_post_type',
'update_post_meta_cache' => false, 'wpv_shell_log' => 'wpvulnerability_register_shell_log_post_type',
'update_post_term_cache' => false, )
'suppress_filters' => false, as $log_post_type => $log_register_callback
); ) {
if ( ! post_type_exists( $log_post_type ) && function_exists( $log_register_callback ) ) {
while ( true ) { call_user_func( $log_register_callback );
$log_ids = get_posts( $log_query_args );
if ( empty( $log_ids ) ) {
break;
} }
foreach ( $log_ids as $log_id ) { $log_query_args = array(
wp_delete_post( (int) $log_id, true ); 'post_type' => $log_post_type,
'fields' => 'ids',
'post_status' => 'any',
'posts_per_page' => 100,
'orderby' => 'ID',
'order' => 'ASC',
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'suppress_filters' => false,
);
while ( true ) {
$log_ids = get_posts( $log_query_args );
if ( empty( $log_ids ) ) {
break;
}
foreach ( $log_ids as $log_id ) {
wp_delete_post( (int) $log_id, true );
}
} }
} }
@ -734,7 +757,7 @@ function wpvulnerability_counter_plugins() {
global $menu; global $menu;
foreach ( $menu as $key => $value ) { foreach ( $menu as $key => $value ) {
if ( 'plugins.php' === $menu[ $key ][2] ) { if ( 'plugins.php' === $menu[ $key ][2] ) {
$menu[ $key ][0] .= ' <span class="update-plugins" style="background-color: #FFD700; color: #000000;"><span class="update-count" title="' . __( 'Vulnerabilities', 'wpvulnerability' ) . '">' . esc_html( (string) $wpvulnerability_plugins_total ) . '</span></span>'; // phpcs:ignore $menu[ $key ][0] .= ' <span class="update-plugins" style="background-color: #FFD700; color: #000000;"><span class="update-count" title="' . esc_html__( 'Vulnerabilities', 'wpvulnerability' ) . '">' . esc_html( (string) $wpvulnerability_plugins_total ) . '</span></span>'; // phpcs:ignore
break; break;
} }
} }
@ -839,7 +862,7 @@ function wpvulnerability_counter_core() {
if ( isset( $submenu['index.php'] ) ) { if ( isset( $submenu['index.php'] ) ) {
foreach ( $submenu['index.php'] as $key => $value ) { foreach ( $submenu['index.php'] as $key => $value ) {
if ( 'update-core.php' === $submenu['index.php'][ $key ][2] ) { if ( 'update-core.php' === $submenu['index.php'][ $key ][2] ) {
$submenu['index.php'][ $key ][0] .= ' <span class="update-plugins" style="background-color: #FFD700; color: #000000;"><span class="update-count" title="' . __( 'Vulnerabilities', 'wpvulnerability' ) . '">' . esc_html( (string) $wpvulnerability_core_total ) . '</span></span>'; // phpcs:ignore $submenu['index.php'][ $key ][0] .= ' <span class="update-plugins" style="background-color: #FFD700; color: #000000;"><span class="update-count" title="' . esc_html__( 'Vulnerabilities', 'wpvulnerability' ) . '">' . esc_html( (string) $wpvulnerability_core_total ) . '</span></span>'; // phpcs:ignore
break; break;
} }
} }

View file

@ -37,20 +37,55 @@ function wpvulnerability_add_every_six_hours( $schedules ) {
return $schedules; return $schedules;
} }
// Add weekly and daily schedules used by the notification cron.
// Registered here because this file is always loaded, while the notification
// scheduling that resolves them runs on init for every request type.
add_filter( 'cron_schedules', 'wpvulnerability_add_every_week' );
add_filter( 'cron_schedules', 'wpvulnerability_add_every_day' );
/**
* Registers a custom weekly cron schedule.
*
* @since 2.0.0
*
* @param array<string, array<string, int|string>> $schedules Existing schedules.
* @return array<string, array<string, int|string>> Schedules with the weekly interval added.
*/
function wpvulnerability_add_every_week( $schedules ) {
$schedules['weekly'] = array(
'interval' => 604800,
'display' => did_action( 'init' ) ? __( 'Every week', 'wpvulnerability' ) : 'Every week',
);
return $schedules;
}
/**
* Registers a custom daily cron schedule.
*
* @since 2.0.0
*
* @param array<string, array<string, int|string>> $schedules Existing schedules.
* @return array<string, array<string, int|string>> Schedules with the daily interval added.
*/
function wpvulnerability_add_every_day( $schedules ) {
$schedules['daily'] = array(
'interval' => 86400,
'display' => did_action( 'init' ) ? __( 'Every day', 'wpvulnerability' ) : 'Every day',
);
return $schedules;
}
// Remove legacy scheduled events on subsites in multisite installs. // Remove legacy scheduled events on subsites in multisite installs.
if ( is_multisite() && ! is_main_site() ) { if ( is_multisite() && ! is_main_site() ) {
wpvulnerability_clear_plugin_cron_hooks(); wpvulnerability_clear_plugin_cron_hooks();
} }
/** // Schedule the automatic vulnerability database updates on init. Scheduling at
* Schedule Automatic Vulnerability Database Update. // file scope called wp_get_schedules() before init, firing other plugins'
* If the 'wpvulnerability_update_database' event is not already scheduled, schedule it to run twice daily. // cron_schedules callbacks (and their __() calls) too early on WordPress 6.7+.
* add_action( 'init', 'wpvulnerability_schedule_core_events', 20 );
* @since 2.0.0
*
* @return void
*/
wpvulnerability_schedule_core_events();
// Hook the event to the function that updates the database. // Hook the event to the function that updates the database.
add_action( 'wpvulnerability_update_database', 'wpvulnerability_update_database_data' ); add_action( 'wpvulnerability_update_database', 'wpvulnerability_update_database_data' );
@ -138,16 +173,35 @@ function wpvulnerability_schedule_notification_event( $config, $force = true ) {
if ( ! is_multisite() || is_main_site() ) { if ( ! is_multisite() || is_main_site() ) {
$period_val = $config['period']; $period_val = $config['period'];
$period_str = is_scalar( $period_val ) ? (string) $period_val : ''; $period_str = is_scalar( $period_val ) ? (string) $period_val : '';
$timestamp = wpvulnerability_get_next_notification_timestamp( $config );
// Whitelist the period: a corrupted value would make the event unschedulable.
if ( ! in_array( $period_str, array( 'daily', 'weekly' ), true ) ) {
return;
}
$timestamp = wpvulnerability_get_next_notification_timestamp( $config );
wp_schedule_event( $timestamp, $period_str, 'wpvulnerability_notification' ); wp_schedule_event( $timestamp, $period_str, 'wpvulnerability_notification' );
} }
} }
$wpvulnerability_s_raw = is_multisite() ? get_site_option( 'wpvulnerability-config' ) : get_option( 'wpvulnerability-config' );
$wpvulnerability_s = is_array( $wpvulnerability_s_raw ) ? $wpvulnerability_s_raw : array();
wpvulnerability_schedule_notification_event( $wpvulnerability_s, false );
add_action( 'wpvulnerability_notification', 'wpvulnerability_execute_notification' ); // @phpstan-ignore return.void (function returns bool but action callbacks are not required to be void) add_action( 'wpvulnerability_notification', 'wpvulnerability_execute_notification' ); // @phpstan-ignore return.void (function returns bool but action callbacks are not required to be void)
unset( $wpvulnerability_s );
/**
* Schedules the notification event on init.
*
* Runs at init so wp_get_schedules() is never called before translations can load.
*
* @since 5.1.3
*
* @return void
*/
function wpvulnerability_schedule_notification_boot() {
$config_raw = is_multisite() ? get_site_option( 'wpvulnerability-config' ) : get_option( 'wpvulnerability-config' );
$config = is_array( $config_raw ) ? $config_raw : array();
wpvulnerability_schedule_notification_event( $config, false );
}
add_action( 'init', 'wpvulnerability_schedule_notification_boot', 20 );
/** /**
* Returns the WPVulnerability cron hooks. * Returns the WPVulnerability cron hooks.

View file

@ -182,6 +182,9 @@ function wpvulnerability_test_software( $software ) {
'mysql' => __( 'MySQL', 'wpvulnerability' ), 'mysql' => __( 'MySQL', 'wpvulnerability' ),
'imagemagick' => __( 'ImageMagick', 'wpvulnerability' ), 'imagemagick' => __( 'ImageMagick', 'wpvulnerability' ),
'curl' => __( 'curl', 'wpvulnerability' ), 'curl' => __( 'curl', 'wpvulnerability' ),
'memcached' => __( 'memcached', 'wpvulnerability' ),
'redis' => __( 'Redis', 'wpvulnerability' ),
'sqlite' => __( 'SQLite', 'wpvulnerability' ),
); );
if ( ! array_key_exists( $software, $software_list ) ) { if ( ! array_key_exists( $software, $software_list ) ) {
@ -488,4 +491,10 @@ function wpvulnerability_tests( $tests ) {
} }
// Adds the vulnerability tests to the site status tests. // Adds the vulnerability tests to the site status tests.
// Site Health exists since WordPress 5.2. Core loads the WP_Site_Health class
// lazily (only inside site-health.php, health-check AJAX, and the dashboard
// widget), long after init, so class_exists() cannot be used here: it would
// always be false and the integration would never register. Registering
// unconditionally is safe on every version - below 5.2 the
// 'site_status_tests' filter simply never fires.
add_filter( 'site_status_tests', 'wpvulnerability_tests' ); add_filter( 'site_status_tests', 'wpvulnerability_tests' );

View file

@ -221,7 +221,7 @@ function wpvulnerability_get_installed( $software ) {
// Return the JSON-encoded array of software data. // Return the JSON-encoded array of software data.
$encoded = wp_json_encode( $data ); $encoded = wp_json_encode( $data );
return false !== $encoded ? $encoded : ''; return false !== $encoded ? $encoded : '[]';
} }
/** /**

View file

@ -687,8 +687,9 @@ function wpvulnerability_themes_view( $views ) {
return $views; return $views;
} }
$raw_count = is_multisite() ? get_site_option( 'wpvulnerability-themes-vulnerable', '0' ) : '0'; $raw_count = is_multisite() ? get_site_option( 'wpvulnerability-themes-vulnerable', '0' ) : get_option( 'wpvulnerability-themes-vulnerable', '0' );
$wpvulnerability_themes_total = ( is_scalar( json_decode( is_string( $raw_count ) ? $raw_count : '0', true ) ) ? (int) json_decode( is_string( $raw_count ) ? $raw_count : '0', true ) : 0 ); $decoded_count = json_decode( is_string( $raw_count ) ? $raw_count : '0', true );
$wpvulnerability_themes_total = ( is_scalar( $decoded_count ) ? (int) $decoded_count : 0 );
if ( is_multisite() && is_network_admin() ) { if ( is_multisite() && is_network_admin() ) {
$url = network_admin_url( 'themes.php?theme_status=vulnerable' ); $url = network_admin_url( 'themes.php?theme_status=vulnerable' );

View file

@ -3,9 +3,9 @@
* Plugin Name: WPVulnerability * Plugin Name: WPVulnerability
* Plugin URI: https://www.wpvulnerability.com/plugin/ * Plugin URI: https://www.wpvulnerability.com/plugin/
* Description: Receive information about possible vulnerabilities in your WordPress from WordPress Vulnerability Database API. * Description: Receive information about possible vulnerabilities in your WordPress from WordPress Vulnerability Database API.
* Requires at least: 5.6 * Requires at least: 4.7
* Requires PHP: 7.0 * Requires PHP: 7.0
* Version: 5.1.0 * Version: 5.1.6
* Author: ROBOTSTXT * Author: ROBOTSTXT
* Author URI: https://www.robotstxt.es/ * Author URI: https://www.robotstxt.es/
* License: GPL-3.0-or-later * License: GPL-3.0-or-later
@ -16,7 +16,7 @@
* *
* @package WPVulnerability * @package WPVulnerability
* *
* @version 5.1.0 * @version 5.1.6
*/ */
defined( 'ABSPATH' ) || die( 'No script kiddies please!' ); defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
@ -24,7 +24,7 @@ defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
/** /**
* Set some constants that I can change in future versions. * Set some constants that I can change in future versions.
*/ */
define( 'WPVULNERABILITY_PLUGIN_VERSION', '5.1.0' ); define( 'WPVULNERABILITY_PLUGIN_VERSION', '5.1.6' );
define( 'WPVULNERABILITY_API_HOST', 'https://www.wpvulnerability.net/' ); define( 'WPVULNERABILITY_API_HOST', 'https://www.wpvulnerability.net/' );
/** /**
@ -35,6 +35,14 @@ define( 'WPVULNERABILITY_PLUGIN_FILE', __FILE__ );
define( 'WPVULNERABILITY_PLUGIN_BASE', plugin_basename( __FILE__ ) ); define( 'WPVULNERABILITY_PLUGIN_BASE', plugin_basename( __FILE__ ) );
define( 'WPVULNERABILITY_PLUGIN_PATH', plugin_dir_path( __FILE__ ) ); define( 'WPVULNERABILITY_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );
/*
* WordPress core function polyfills (wp_date, wp_timezone, wp_doing_cron, ...).
* Loaded first so every module can call the core functions unconditionally.
*
* @since 5.1.3
*/
require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-compat.php';
// Handle front-end email opt-out requests early. // Handle front-end email opt-out requests early.
if ( isset( $_GET['wpvulnerability_disable_email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['wpvulnerability_disable_email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
// Load pluggable functions so wp_verify_nonce() is available. // Load pluggable functions so wp_verify_nonce() is available.
@ -126,7 +134,8 @@ if (
require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-run.php'; require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-run.php';
register_activation_hook( WPVULNERABILITY_PLUGIN_FILE, 'wpvulnerability_activation' ); register_activation_hook( WPVULNERABILITY_PLUGIN_FILE, 'wpvulnerability_activation' );
register_deactivation_hook( WPVULNERABILITY_PLUGIN_FILE, 'wpvulnerability_deactivation' ); register_deactivation_hook( WPVULNERABILITY_PLUGIN_FILE, 'wpvulnerability_deactivation' );
register_uninstall_hook( WPVULNERABILITY_PLUGIN_FILE, 'wpvulnerability_uninstall' ); // Uninstall is handled by uninstall.php (it supersedes register_uninstall_hook
// and enforces the delete_on_uninstall opt-in); see docs/known-issues.md KI-3.
add_action( 'init', 'wpvulnerability_plugin_init' ); add_action( 'init', 'wpvulnerability_plugin_init' );
} }
@ -137,3 +146,19 @@ if (
*/ */
require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-schedule.php'; require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-schedule.php';
require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-api.php'; require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-api.php';
/**
* Load plugin translations on init.
*
* Explicitly loading the textdomain on init prevents the
* "_load_textdomain_just_in_time() was called incorrectly" notice
* on WordPress 6.7+.
*
* @since 5.1.2
*
* @return void
*/
function wpvulnerability_load_textdomain() {
load_plugin_textdomain( 'wpvulnerability', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
}
add_action( 'init', 'wpvulnerability_load_textdomain' );