File: /home/emerlux/public_html/wp-content/plugins/wpvulnerability/wpvulnerability-general.php
<?php
/**
* General functions
*
* @package WPVulnerability
*
* @version 2.0.0
*/
defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
/**
* Clear the existing cache for the specified type.
*
* @since 2.0.0
*
* @param string $type The type of cache to clear (core, plugins, themes).
* @return void
*/
function wpvulnerability_clear_cache( $type ) {
if ( is_multisite() ) {
delete_site_option( "wpvulnerability-{$type}" );
delete_site_option( "wpvulnerability-{$type}-vulnerable" );
delete_site_option( "wpvulnerability-{$type}-cache" );
} else {
delete_option( "wpvulnerability-{$type}" );
delete_option( "wpvulnerability-{$type}-vulnerable" );
delete_option( "wpvulnerability-{$type}-cache" );
}
}
/**
* Checks and validates user capabilities for managing vulnerability settings in a WordPress environment.
*
* This function verifies if the current user has the appropriate permissions to manage network settings
* in a multisite installation or manage options in a single site installation. It ensures that only
* Administrators in a single site and Super Administrators in multisite can access these settings.
*
* @since 3.0.0
*
* @return bool Returns true if the current user has the required capabilities, false otherwise.
*/
function wpvulnerability_capabilities() {
// Check if the user is logged in.
if ( ! is_user_logged_in() ) {
return false;
}
// Check if in a Multisite environment.
if ( is_multisite() && is_super_admin() && ( is_network_admin() || is_main_site() ) ) {
return true;
} elseif ( is_admin() && current_user_can( 'manage_options' ) ) {
return true;
}
// Return false if the user does not have the required capabilities.
return false;
}
/**
* Checks if the `shell_exec` function can be used.
*
* This function verifies if the `shell_exec` function is not disabled in the server's
* configuration and is able to execute a basic shell command. It also checks for
* safe mode, which is relevant for older PHP versions before 5.4.
*
* @since 3.4.0
*
* @return bool True if `shell_exec` is available and working, false otherwise.
*/
function wpvulnerability_can_shell_exec() {
// Check if `shell_exec` is disabled.
if ( in_array( 'shell_exec', array_map( 'trim', explode( ',', ini_get( 'disable_functions' ) ) ), true ) ) {
return false; // `shell_exec` is disabled.
}
// Try to execute a simple command to confirm functionality.
$test = shell_exec( 'echo test' ); // phpcs:ignore
// If the command execution failed or returned null, shell_exec is not working.
return null !== $test; // Return true if the command was successful.
}
/**
* Retrieve the cache expiration in hours.
*
* The value can be defined via the WPVULNERABILITY_CACHE_HOURS constant,
* configured in the plugin settings, or falls back to 12 hours.
*
* @since 4.1.0
*
* @return int Cache duration in hours.
*/
function wpvulnerability_cache_hours() {
$default = 12;
if ( defined( 'WPVULNERABILITY_CACHE_HOURS' ) && WPVULNERABILITY_CACHE_HOURS !== $default ) {
return (int) WPVULNERABILITY_CACHE_HOURS;
}
$settings = is_multisite() ? get_site_option( 'wpvulnerability-config', array() ) : get_option( 'wpvulnerability-config', array() );
if ( isset( $settings['cache'] ) ) {
$cache = (int) $settings['cache'];
if ( in_array( $cache, array( 1, 6, 12, 24 ), true ) ) {
return $cache;
}
}
return $default;
}
/**
* Normalize various truthy and falsy values into the expected 'y' or 'n' format.
*
* This helper ensures that configuration options stored as booleans or integers
* in previous plugin versions are converted into the new string-based format.
*
* @since 4.1.1
*
* @param mixed $value Value to normalize.
*
* @return string Returns 'y' when enabled, 'n' otherwise.
*/
function wpvulnerability_normalize_yes_no( $value ) {
if ( is_string( $value ) ) {
$value = strtolower( trim( $value ) );
}
$truthy = array( 'y', 'yes', '1', 1, true, 'true', 'on' );
return in_array( $value, $truthy, true ) ? 'y' : 'n';
}
/**
* Determine if a stored yes/no value should be treated as enabled.
*
* @since 4.1.1
*
* @param mixed $value Value to evaluate.
*
* @return bool True when enabled, false otherwise.
*/
function wpvulnerability_is_yes( $value ) {
return 'y' === wpvulnerability_normalize_yes_no( $value );
}
/**
* Normalize the notification configuration array.
*
* @since 4.1.1
*
* @param mixed $notify Notification configuration values.
*
* @return array Normalized notification configuration containing 'email', 'slack', and 'teams'.
*/
function wpvulnerability_normalize_notify_settings( $notify ) {
$defaults = array(
'email' => 'n',
'slack' => 'n',
'teams' => 'n',
);
$normalized = array();
if ( is_array( $notify ) ) {
foreach ( $notify as $channel => $value ) {
$normalized[ $channel ] = wpvulnerability_normalize_yes_no( $value );
}
}
return array_merge( $defaults, $normalized );
}
/**
* Sanitize a version string.
*
* This function removes any leading or trailing whitespace from the version string
* and strips out any non-alphanumeric characters except for hyphens, underscores, and dots.
*
* @version 2.0.0
*
* @param string $version The version string to sanitize.
*
* @return string The sanitized version string.
*/
function wpvulnerability_sanitize_version( $version ) {
// Remove any leading or trailing whitespace.
$version = trim( (string) $version );
// Strip out any non-alphanumeric characters except for hyphens, underscores, and dots.
$version = preg_replace( '/[^a-zA-Z0-9_\-.]+/', '', $version );
return $version;
}
/**
* Sanitize a version string and validate its format.
*
* This function sanitizes the input version string and checks it against a regular expression
* to match the standard versioning format (major.minor[.patch[.build]]). It returns the matched version
* if it conforms to the expected format; otherwise, it returns the original version.
*
* @since 3.5.0 Introduced.
*
* @param string $version The version string to sanitize and validate.
* @return string The sanitized version string if it matches the standard format; otherwise, the original version string.
*/
function wpvulnerability_sanitize_and_validate_version( $version ) {
// Sanitize the version string using the base sanitizer.
$version = wpvulnerability_sanitize_version( $version );
// Validate format (major.minor[.patch[.build]]) and sanitize.
if ( preg_match( '/^\d+\.\d+(\.\d+){0,2}(\.\d+)?/', $version, $match ) ) {
if ( isset( $match[0] ) ) {
return trim( $match[0] );
}
}
return $version;
}
/**
* Detects the version of SQLite using the SQLite3 extension or system commands.
*
* @since 3.5.0 Introduced.
*
* @return string|null The version of SQLite in the format N.n.n, N.n, etc., or null if it cannot be detected.
*/
function wpvulnerability_detect_sqlite() {
// Initialize the version variable.
$version = null;
// First method: use the SQLite3 extension of PHP.
if ( class_exists( 'SQLite3' ) ) {
$sqlite = new SQLite3( ':memory:' ); // Create an in-memory SQLite database.
$version_info = $sqlite->version();
if ( isset( $version_info['versionString'] ) ) {
$version = $version_info['versionString'];
// Replace "-N" at the end with ".N" if present.
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
}
}
}
// Second method: use system commands if the first fails and shell_exec is available.
if ( empty( $version ) && wpvulnerability_can_shell_exec() ) {
// Command to check SQLite version.
$version_output = shell_exec( escapeshellcmd( 'sqlite3 --version' ) ); // phpcs:ignore
if ( ! empty( $version_output ) && preg_match( '/(\d+\.\d+(?:\.\d+)?(?:-\d+)?)/', $version_output, $matches ) ) {
$version = $matches[1];
// Replace "-N" at the end with ".N" if present.
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
}
}
}
// Return the sanitized and validated version or null if it cannot be detected.
return wpvulnerability_sanitize_and_validate_version( $version );
}
/**
* Detects the version of Redis using the Redis extension or system commands.
*
* @since 3.5.0 Introduced.
*
* @return string|null The version of Redis in the format N.n.n, N.n, etc., or null if it cannot be detected.
*/
function wpvulnerability_detect_redis() {
// Initialize the version variable.
$version = null;
// First method: use the Redis extension of PHP.
if ( class_exists( 'Redis' ) ) {
$redis = new Redis();
try {
$redis_info = $redis->info();
if ( isset( $redis_info['redis_version'] ) ) {
$version = $redis_info['redis_version'];
// Replace "-N" at the end with ".N" if present.
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
}
}
} catch ( RedisException $e ) {
// There is the PHP extension, but no Redis.
unset( $redis );
}
}
// Second method: use system commands if the first fails and shell_exec is available.
if ( empty( $version ) && wpvulnerability_can_shell_exec() ) {
// Command to check Redis version.
$version_output = shell_exec( escapeshellcmd( 'redis-server --version' ) ); // phpcs:ignore
if ( ! empty( $version_output ) && preg_match( '/redis-server\s+v=([\d.]+)/i', $version_output, $matches ) ) {
$version = $matches[1];
// Replace "-N" at the end with ".N" if present.
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
}
}
}
// Return the sanitized and validated version or null if it cannot be detected.
return wpvulnerability_sanitize_and_validate_version( $version );
}
/**
* Detects the version of Memcached using the Memcached extension or system commands.
*
* @since 3.5.0 Introduced.
*
* @return string|null The version of Memcached in the format N.n.n, N.n, etc., or null if it cannot be detected.
*/
function wpvulnerability_detect_memcached() {
// Initialize the version variable.
$version = null;
// First method: use the Memcached extension of PHP.
if ( class_exists( 'Memcached' ) ) {
$memcached = new Memcached();
try {
$version_info = $memcached->getVersion();
if ( is_array( $version_info ) && isset( $version_info[0] ) ) {
// The version is usually the first element in the returned array.
$version = $version_info[0];
// Replace "-N" at the end with ".N" if present.
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
}
}
} catch ( MemcachedException $e ) {
// There is the PHP extension, but no memcached.
unset( $memcached );
}
}
// Second method: use system commands if the first fails and shell_exec is available.
if ( empty( $version ) && wpvulnerability_can_shell_exec() ) {
// Command to check Memcached version.
$version_output = shell_exec( escapeshellcmd( 'memcached -h' ) ); // phpcs:ignore
if ( ! empty( $version_output ) && preg_match( '/memcached\s+(\d+\.\d+(?:\.\d+)?(?:-\d+)?)/i', $version_output, $matches ) ) {
$version = $matches[1];
// Replace "-N" at the end with ".N" if present.
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
}
}
}
// Return the sanitized and validated version or null if it cannot be detected.
return wpvulnerability_sanitize_and_validate_version( $version );
}
/**
* Detects the PHP version using the phpversion function or system commands.
*
* @since 3.5.0 Introduced.
*
* @return string|null The version of PHP in the format N.n.n, N.n, etc., or null if it cannot be detected.
*/
function wpvulnerability_detect_php() {
// Initialize the version variable.
$version = null;
// First method: use the PHP_VERSION constant.
if ( defined( 'PHP_VERSION' ) ) {
$version = PHP_VERSION;
}
// First method: use the phpversion function.
if ( empty( $version ) && function_exists( 'phpversion' ) ) {
$version = phpversion();
}
// Second method: use system commands if the first fails and shell_exec is available.
if ( empty( $version ) && wpvulnerability_can_shell_exec() ) {
// Command to check PHP version.
$version_output = shell_exec( escapeshellcmd( 'php -v' ) ); // phpcs:ignore
if ( ! empty( $version_output ) && preg_match( '/PHP\s+(\d+\.\d+(?:\.\d+)?(?:-\d+)?)/i', $version_output, $matches ) ) {
$version = $matches[1];
// Replace "-N" at the end with ".N" if present.
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
}
}
}
// Return the sanitized and validated PHP version or null if it cannot be detected.
return wpvulnerability_sanitize_and_validate_version( $version );
}
/**
* Detects the version of cURL using the cURL extension or system commands.
*
* @since 3.5.0 Introduced.
*
* @return string|null The version of cURL in the format N.n.n, N.n, etc., or null if it cannot be detected.
*/
function wpvulnerability_detect_curl() {
// Product name for consistency.
$version = null;
// First method: use the cURL extension of PHP.
if ( function_exists( 'curl_version' ) ) {
$curl_info = curl_version();
$version = isset( $curl_info['version'] ) ? $curl_info['version'] : null;
}
// Second method: use system commands if the first fails and shell_exec is available.
if ( empty( $version ) && wpvulnerability_can_shell_exec() ) {
// Command to check cURL version.
$version_output = shell_exec( escapeshellcmd( 'curl --version' ) ); // phpcs:ignore
if ( ! empty( $version_output ) && preg_match( '/curl\s+(\d+\.\d+(?:\.\d+)?(?:-\d+)?)/i', $version_output, $matches ) ) {
$version = $matches[1];
// Replace "-N" at the end with ".N" if present.
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
}
}
}
// Return the sanitized and validated version or null if it cannot be detected.
return wpvulnerability_sanitize_and_validate_version( $version );
}
/**
* Detects the version of ImageMagick using the Imagick extension or system commands.
*
* @since 3.5.0 Introduced.
*
* @return string|null The version of ImageMagick in the format N.n.n, N.n, etc., or null if it cannot be detected.
*/
function wpvulnerability_detect_imagemagick() {
// Product name to avoid repetition and facilitate future changes.
$version = null;
// First method: use the Imagick extension of PHP.
if ( extension_loaded( 'imagick' ) && class_exists( 'Imagick' ) ) {
$imagick = new Imagick();
$version_info = $imagick->getVersion();
if ( isset( $version_info['versionString'] ) ) {
// Extract the version using a regular expression.
if ( preg_match( '/ImageMagick\s+(\d+\.\d+(?:\.\d+)?(?:-\d+)?)/i', $version_info['versionString'], $matches ) ) {
$version = $matches[1];
// Replace "-N" at the end with ".N" if present.
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
}
}
}
}
// Second method: use system commands if the first fails and shell_exec is available.
if ( empty( $version ) && wpvulnerability_can_shell_exec() ) {
$commands = array( 'magick -version', 'convert -version' );
foreach ( $commands as $cmd ) {
// Execute the command securely.
$version_output = shell_exec( escapeshellcmd( $cmd ) ); // phpcs:ignore
if ( ! empty( $version_output ) && preg_match( '/ImageMagick\s+(\d+\.\d+(?:\.\d+)?(?:-\d+)?)/i', $version_output, $matches ) ) {
$version = $matches[1];
// Replace "-N" at the end with ".N" if present.
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
}
break; // Exit the loop once the version is found.
}
}
}
// Return the version or null if it cannot be detected.
return wpvulnerability_sanitize_and_validate_version( $version );
}
/**
* Detects the web server software and version from the SERVER_SOFTWARE server variable.
*
* This function attempts to identify the web server software (e.g., Apache, nginx) and its version
* based on the 'SERVER_SOFTWARE' environment variable provided by the server. It uses regular expressions
* to parse the web server name and version. The function also sanitizes the detected version number
* to a standard format (major.minor.patch).
*
* @since 3.2.0 Introduced.
*
* @return array Returns an associative array with three keys:
* 'id' => A short, lowercase identifier for the web server (e.g., 'apache', 'nginx'),
* 'name' => A more readable name for the web server (e.g., 'Apache HTTPD', 'nginx'),
* 'version' => The detected version of the web server, sanitized to a standard format.
*/
function wpvulnerability_detect_webserver() {
// Initialize an array to hold the web server information.
$webserver = array(
'id' => null,
'name' => null,
'version' => null,
);
// Check if the SERVER_SOFTWARE variable is set.
if ( isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
// Trim and sanitize the server software string.
$webserver_software = trim( wp_kses( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ), 'strip' ) );
// Use regular expressions to extract the web server name and version.
if ( preg_match( '/^(\w+)\/?([^\s]*)/', $webserver_software, $matches ) ) {
$webserver['name'] = isset( $matches[1] ) ? trim( $matches[1] ) : null;
$webserver['version'] = isset( $matches[2] ) ? trim( $matches[2] ) : null;
// Replace "-N" at the end of the version with ".N" if present.
if ( preg_match( '/-(\d+)$/', $webserver['version'], $suffix_matches ) ) {
$webserver['version'] = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $webserver['version'] );
}
}
}
// Normalize and set the web server ID based on the detected name.
if ( ! empty( $webserver['name'] ) ) {
$webserver['id'] = strtolower( $webserver['name'] );
switch ( $webserver['id'] ) {
case 'httpd':
case 'apache':
$webserver['id'] = 'apache';
$webserver['name'] = 'Apache HTTPD';
break;
case 'nginx':
$webserver['id'] = 'nginx';
$webserver['name'] = 'nginx';
break;
// Additional web servers can be added here.
}
}
// If the version is not detected, try to get it from the OS.
if ( empty( $webserver['version'] ) && wpvulnerability_can_shell_exec() ) {
if ( 'apache' === $webserver['id'] ) {
$apache_version = shell_exec( 'apache2 -v 2>&1' ) ?: shell_exec( 'httpd -v 2>&1' ); // phpcs:ignore
if ( preg_match( '/Apache\/([\d.]+)/', $apache_version, $version_matches ) ) {
$webserver['version'] = $version_matches[1];
// Replace "-N" at the end with ".N" if present.
if ( preg_match( '/-(\d+)$/', $webserver['version'], $suffix_matches ) ) {
$webserver['version'] = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $webserver['version'] );
}
}
} elseif ( 'nginx' === $webserver['id'] ) {
$nginx_version = shell_exec( 'nginx -v 2>&1' ); // phpcs:ignore
if ( preg_match( '/nginx\/([\d.]+)/', $nginx_version, $version_matches ) ) {
$webserver['version'] = $version_matches[1];
// Replace "-N" at the end with ".N" if present.
if ( preg_match( '/-(\d+)$/', $webserver['version'], $suffix_matches ) ) {
$webserver['version'] = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $webserver['version'] );
}
} else {
$angie_version = shell_exec( 'angie -v 2>&1' ); // phpcs:ignore
if ( preg_match( '/angie\/([\d.]+)/', $angie_version, $version_matches ) ) {
$webserver['version'] = $version_matches[1];
// Replace "-N" at the end with ".N" if present.
if ( preg_match( '/-(\d+)$/', $webserver['version'], $suffix_matches ) ) {
$webserver['version'] = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $webserver['version'] );
}
}
}
}
}
// Sanitize and validate the web server version format.
if ( ! empty( $webserver['version'] ) ) {
// Sanitize the version number to ensure it's in a 'major.minor.patch' format.
$webserver['version'] = wpvulnerability_sanitize_and_validate_version( $webserver['version'] );
}
// Return the detected web server information.
return $webserver;
}
/**
* Detects the SQL server software and version from the database server.
*
* This function identifies the SQL server software (e.g., MariaDB, MySQL) and its version
* by querying the database using the 'SHOW VARIABLES' command. It parses the server name
* and version using the results and sanitizes the detected version number to a standard format (major.minor.patch).
*
* @since 3.4.0
*
* @return array Returns an associative array with three keys:
* 'id' => A short, lowercase identifier for the SQL server (e.g., 'mariadb', 'mysql'),
* 'name' => A more readable name for the SQL server (e.g., 'MariaDB', 'MySQL'),
* 'version' => The detected version of the SQL server, sanitized to a standard format.
*/
function wpvulnerability_detect_sqlserver() {
// Initialize an array to hold the SQL server information.
$sqlserver = array(
'id' => null,
'name' => null,
'version' => null,
);
global $wpdb;
// Query to get the database server type (version_comment).
$database_results = $wpdb->get_results( $wpdb->prepare( 'SHOW VARIABLES LIKE %s', 'version_comment' ) ); // phpcs:ignore
if ( $wpdb->last_error && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
do_action( 'wpdb_last_error', $wpdb->last_error );
}
// Process the results to determine the database type.
if ( ! empty( $database_results ) && isset( $database_results[0]->Value ) ) {
$possible_database = trim( $database_results[0]->Value );
if ( false !== stripos( $possible_database, 'mariadb' ) ) {
$sqlserver['id'] = 'mariadb';
$sqlserver['name'] = 'MariaDB';
} elseif ( false !== stripos( $possible_database, 'mysql' ) ) {
$sqlserver['id'] = 'mysql';
$sqlserver['name'] = 'MySQL';
}
}
// Query to get the database server version.
$version_results = $wpdb->get_results( $wpdb->prepare( 'SHOW VARIABLES LIKE %s', 'version' ) ); // phpcs:ignore
if ( $wpdb->last_error && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
do_action( 'wpdb_last_error', $wpdb->last_error );
}
// Process the results to determine the database version.
if ( ! empty( $version_results ) && isset( $version_results[0]->Value ) ) {
$possible_version = trim( $version_results[0]->Value );
// Sanitize and set the version based on the detected server type.
if ( 'mariadb' === $sqlserver['id'] ) {
// Handle MariaDB versions, considering the format changes and potential '-N' suffix.
if ( preg_match( '/(\d+\.\d+\.\d+(?:-\d+)?)/', $possible_version, $match ) ||
preg_match( '/(\d+\.\d+(?:-\d+)?)/', $possible_version, $match ) ) {
$sqlserver['version'] = $match[1];
}
} elseif ( 'mysql' === $sqlserver['id'] ) {
// Handle MySQL versions, considering potential '-N' suffix.
if ( preg_match( '/(\d+\.\d+\.\d+(?:-\d+)?)/', $possible_version, $match ) ||
preg_match( '/(\d+\.\d+(?:-\d+)?)/', $possible_version, $match ) ) {
$sqlserver['version'] = $match[1];
} else {
// Fallback to the entire version string if regex doesn't match.
$sqlserver['version'] = $possible_version;
}
}
}
// Replace "-N" at the end with ".N" if present.
if ( ! empty( $sqlserver['version'] ) && preg_match( '/-(\d+)$/', $sqlserver['version'], $suffix_matches ) ) {
$sqlserver['version'] = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $sqlserver['version'] );
}
// Sanitize and validate the version format.
if ( ! empty( $sqlserver['version'] ) ) {
$sqlserver['version'] = wpvulnerability_sanitize_and_validate_version( $sqlserver['version'] );
}
// Return the detected SQL server information.
return $sqlserver;
}
/**
* Returns a human-readable HTML entity for the given comparison operator.
*
* This function takes a comparison operator in string format and returns
* its corresponding HTML entity for better readability in web contexts.
*
* @version 2.0.0
*
* @param string $op The operator string to prettify.
*
* @return string The pretty operator HTML string.
*/
function wpvulnerability_pretty_operator( $op ) {
// Normalize the operator string to lowercase and trim whitespace.
$op = trim( strtolower( $op ) );
// Define an associative array mapping operators to their HTML entities.
$operator_map = array(
'lt' => '< ', // Less than.
'le' => '≤ ', // Less than or equal to.
'gt' => '> ', // Greater than.
'ge' => '≥ ', // Greater than or equal to.
'eq' => '= ', // Equal to.
'ne' => '≠ ', // Not equal to.
);
// Return the corresponding HTML entity, or the original operator if not recognized.
return isset( $operator_map[ $op ] ) ? $operator_map[ $op ] : $op;
}
/**
* Returns a human-readable severity level.
*
* This function takes a severity string and returns a human-readable
* severity level, localized for translation.
*
* @version 2.0.0
*
* @param string $severity The severity string to prettify.
*
* @return string The human-readable severity string.
*/
function wpvulnerability_severity( $severity ) {
// Normalize the severity string to lowercase and trim whitespace.
$severity = trim( strtolower( $severity ) );
// Define an associative array mapping severity codes to their human-readable equivalents.
$severity_map = array(
'n' => __( 'None', 'wpvulnerability' ), // No severity.
'l' => __( 'Low', 'wpvulnerability' ), // Low severity.
'm' => __( 'Medium', 'wpvulnerability' ), // Medium severity.
'h' => __( 'High', 'wpvulnerability' ), // High severity.
'c' => __( 'Critical', 'wpvulnerability' ), // Critical severity.
);
// Return the corresponding human-readable severity, or the original if not recognized.
return isset( $severity_map[ $severity ] ) ? $severity_map[ $severity ] : $severity;
}
/**
* Retrieves vulnerabilities information from the API.
*
* This function fetches vulnerability information based on the provided type and slug.
* It supports caching to minimize API requests and improve performance.
*
* @version 2.0.0
*
* @param string $type The type of vulnerability. Can be 'core', 'plugin', or 'theme'.
* @param string $slug The slug of the plugin or theme. For core vulnerabilities, it is the version string.
* @param int $cache Optional. Whether to use cache. Default is 1 (true).
*
* @return array|bool An array with the vulnerability information or false if there's an error.
*/
function wpvulnerability_get( $type, $slug = '', $cache = 1 ) {
// Validate vulnerability type and normalize.
$type = strtolower( trim( $type ) );
$valid_types = array( 'core', 'plugin', 'theme' );
if ( ! in_array( $type, $valid_types, true ) ) {
wp_die( 'Unknown vulnerability type sent.' );
}
// Validate slug for plugin or theme.
if ( ( 'plugin' === $type || 'theme' === $type ) && empty( sanitize_title( $slug ) ) ) {
return false;
}
// Validate slug for core.
if ( 'core' === $type && ! wpvulnerability_sanitize_version( $slug ) ) {
return false;
}
// Cache key.
$key = 'wpvulnerability_' . $type . '_' . $slug;
// Attempt to retrieve cached data.
$vulnerability_data = $cache ? ( is_multisite() ? get_site_transient( $key ) : get_transient( $key ) ) : null;
// If not cached, fetch updated data.
if ( empty( $vulnerability_data ) ) {
$url = WPVULNERABILITY_API_HOST . $type . '/' . $slug . '/';
$response = wp_remote_get( $url, array( 'timeout' => 2500 ) );
if ( ! is_wp_error( $response ) ) {
$body = wp_remote_retrieve_body( $response );
// Cache the response data.
if ( is_multisite() ) {
set_site_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.
}
}
return json_decode( $vulnerability_data, true );
}
/**
* Retrieve vulnerabilities for a specific version of WordPress Core.
*
* This function fetches vulnerability information for a given version of WordPress Core.
* If no version is provided, it retrieves vulnerabilities for the currently installed version.
* It supports caching to minimize API requests and improve performance.
*
* @since 2.0.0
*
* @param string|null $version The version number of WordPress Core. If null, retrieves for the installed version.
* @param int $cache Optional. Whether to use cache. Default is 1 (true).
*
* @return array|false Array of vulnerabilities, or false on error.
*/
function wpvulnerability_get_core( $version = null, $cache = 1 ) {
// Sanitize the version number.
if ( ! wpvulnerability_sanitize_version( $version ) ) {
$version = null; // Reset version if sanitization fails.
}
// If version number is null, retrieve for the installed version.
if ( is_null( $version ) ) {
$version = get_bloginfo( 'version' );
}
// Get vulnerabilities from the API.
$response = wpvulnerability_get( 'core', $version, $cache );
// Check for errors in the response.
if ( ( isset( $response['error'] ) && $response['error'] ) || empty( $response['data']['vulnerability'] ) ) {
return false;
}
// Process vulnerabilities and return as an array.
$vulnerabilities = array();
foreach ( $response['data']['vulnerability'] as $v ) {
$vulnerabilities[] = array(
'name' => isset( $v['name'] ) ? wp_kses( (string) $v['name'], 'strip' ) : null,
'link' => isset( $v['link'] ) ? esc_url_raw( (string) $v['link'] ) : null,
'source' => isset( $v['source'] ) ? $v['source'] : null,
'impact' => isset( $v['impact'] ) ? $v['impact'] : null,
);
}
return $vulnerabilities;
}
/**
* Determines if a vulnerability applies to the specified version of the plugin.
*
* @since 3.5.0 Introduced.
*
* @param array $v The vulnerability data.
* @param string $version The version of the plugin.
*
* @return bool True if the vulnerability applies, false otherwise.
*/
function wpvulnerability_is_vulnerability_applicable( $v, $version ) {
// Check if the vulnerability has minimum and maximum versions.
if ( isset( $v['operator']['min_operator'], $v['operator']['max_operator'] ) ) {
return version_compare( $version, $v['operator']['min_version'], $v['operator']['min_operator'] ) &&
version_compare( $version, $v['operator']['max_version'], $v['operator']['max_operator'] );
}
// Check if the vulnerability has only a maximum version.
if ( isset( $v['operator']['max_operator'] ) ) {
return version_compare( $version, $v['operator']['max_version'], $v['operator']['max_operator'] );
}
// Check if the vulnerability has only a minimum version.
if ( isset( $v['operator']['min_operator'] ) ) {
return version_compare( $version, $v['operator']['min_version'], $v['operator']['min_operator'] );
}
return false;
}
/**
* Retrieves vulnerabilities for a specified plugin, optionally returning general plugin data.
*
* This function sanitizes the plugin slug and verifies the version number before querying the vulnerability API.
* If `$data` is set to 1, it returns general information about the plugin instead of vulnerabilities.
* The function returns an array of vulnerabilities or plugin data based on the `$data` parameter, or `false`
* if no vulnerabilities are found or the version number is invalid and `$data` is not set.
*
* @since 2.0.0 Introduced.
*
* @param string $slug The slug of the plugin to check for vulnerabilities.
* @param string $version The version of the plugin to check. The function may return `false` if this is invalid and `$data` is not set.
* @param int $data Optional. Set to 1 to return general plugin data instead of vulnerabilities. Default 0 (return vulnerabilities).
* @param int $cache Optional. Whether to use cache. Default is 1 (true).
*
* @return array|false An array of vulnerabilities or plugin data if `$data` is set to 1, or `false` if no vulnerabilities are found or the version number is invalid and `$data` is not set.
*/
function wpvulnerability_get_plugin( $slug, $version, $data = 0, $cache = 1 ) {
// Sanitize the plugin slug.
$slug = sanitize_title( $slug );
// If the version number is invalid, return false unless $data is set.
if ( ! wpvulnerability_sanitize_version( $version ) && ! $data ) {
return false;
}
// Get the response from the vulnerability API.
$response = wpvulnerability_get( 'plugin', $slug, $cache );
// If $data is set to 1, return general plugin data.
if ( 1 === $data && isset( $response['data'] ) ) {
return array(
'name' => wp_kses( (string) $response['data']['name'], 'strip' ),
'link' => esc_url( (string) $response['data']['link'] ),
'latest' => number_format( (int) $response['data']['latest'], 0, '.', '' ),
'closed' => number_format( (int) $response['data']['closed'], 0, '.', '' ),
);
}
// Check for errors or empty vulnerability data.
if ( ( isset( $response['error'] ) && $response['error'] ) || empty( $response['data']['vulnerability'] ) ) {
return false;
}
// Create an empty array to store vulnerabilities.
$vulnerabilities = array();
// Loop through each vulnerability.
foreach ( $response['data']['vulnerability'] as $v ) {
// Check version constraints and add vulnerabilities accordingly.
if ( wpvulnerability_is_vulnerability_applicable( $v, $version ) ) {
$vulnerabilities[] = array(
'name' => wp_kses( (string) $v['name'], 'strip' ),
'description' => wp_kses_post( (string) $v['description'] ),
'versions' => wp_kses(
wpvulnerability_pretty_operator( isset( $v['operator']['min_operator'] ) ? $v['operator']['min_operator'] : '' ) .
( isset( $v['operator']['min_version'] ) ? $v['operator']['min_version'] : '' ) . ' - ' .
wpvulnerability_pretty_operator( isset( $v['operator']['max_operator'] ) ? $v['operator']['max_operator'] : '' ) .
( isset( $v['operator']['max_version'] ) ? $v['operator']['max_version'] : '' ),
'strip'
),
'version' => wp_kses( (string) ( isset( $v['operator']['min_version'] ) ? $v['operator']['min_version'] : $v['operator']['max_version'] ), 'strip' ),
'unfixed' => (int) ( isset( $v['operator']['unfixed'] ) ? $v['operator']['unfixed'] : 0 ),
'closed' => (int) ( isset( $v['operator']['closed'] ) ? $v['operator']['closed'] : 0 ),
'source' => isset( $v['source'] ) ? $v['source'] : null,
'impact' => isset( $v['impact'] ) ? $v['impact'] : null,
);
}
}
return $vulnerabilities;
}
/**
* Get vulnerabilities for a specific theme.
*
* This function retrieves and sanitizes the theme slug and version before querying the vulnerability API.
* It returns an array of vulnerabilities if any are found, or false if there are none.
*
* @since 3.5.0
*
* @param string $slug Slug of the theme.
* @param string $version Version of the theme.
* @param int $cache Optional. Whether to use cache. Default is 1 (true).
*
* @return array|false Returns an array of vulnerabilities, or false if there are none.
*/
function wpvulnerability_get_theme( $slug, $version, $cache = 1 ) {
// Sanitize the theme slug.
$slug = sanitize_title( $slug );
// Validate the version number.
if ( ! wpvulnerability_sanitize_version( $version ) ) {
return false; // Return false if the version is invalid.
}
// Get the response from the vulnerability API.
$response = wpvulnerability_get( 'theme', $slug, $cache );
// Check for errors or empty vulnerability data.
if ( ( isset( $response['error'] ) && $response['error'] ) || empty( $response['data']['vulnerability'] ) ) {
return false;
}
// Process each vulnerability.
$vulnerabilities = array();
foreach ( $response['data']['vulnerability'] as $v ) {
// Check if the version falls within the min and max operator range.
if ( wpvulnerability_is_vulnerability_applicable( $v, $version ) ) {
$vulnerabilities[] = array(
'name' => wp_kses( (string) $v['name'], 'strip' ),
'description' => wp_kses_post( (string) $v['description'] ),
'versions' => wp_kses(
wpvulnerability_pretty_operator( isset( $v['operator']['min_operator'] ) ? $v['operator']['min_operator'] : '' ) .
( isset( $v['operator']['min_version'] ) ? $v['operator']['min_version'] : '' ) . ' - ' .
wpvulnerability_pretty_operator( isset( $v['operator']['max_operator'] ) ? $v['operator']['max_operator'] : '' ) .
( isset( $v['operator']['max_version'] ) ? $v['operator']['max_version'] : '' ),
'strip'
),
'version' => wp_kses(
(string) ( isset( $v['operator']['min_version'] ) ? $v['operator']['min_version'] : $v['operator']['max_version'] ),
'strip'
),
'unfixed' => (int) ( isset( $v['operator']['unfixed'] ) ? $v['operator']['unfixed'] : 0 ),
'closed' => (int) ( isset( $v['operator']['closed'] ) ? $v['operator']['closed'] : 0 ),
'source' => isset( $v['source'] ) ? $v['source'] : null,
'impact' => isset( $v['impact'] ) ? $v['impact'] : null,
);
}
}
return $vulnerabilities;
}
/**
* Get statistics.
*
* Returns an array with statistical information about vulnerabilities and their respective products.
*
* @since 2.0.0
*
* @param int $cache Optional. Whether to use cache. Default is 1 (true).
*
* @return array|false Returns an array with the statistical information if successful, false otherwise.
*/
function wpvulnerability_get_statistics( $cache = 1 ) {
$key = 'wpvulnerability_stats';
// Attempt to get cached statistics.
$vulnerability = $cache ? ( is_multisite() ? get_site_transient( $key ) : get_transient( $key ) ) : null;
// If cached statistics are not available, retrieve them from the API.
if ( empty( $vulnerability ) ) {
$url = WPVULNERABILITY_API_HOST;
$response = wp_remote_get( $url, array( 'timeout' => 2500 ) );
if ( ! is_wp_error( $response ) ) {
$body = wp_remote_retrieve_body( $response );
// Cache the response data.
if ( is_multisite() ) {
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.
}
}
// Decode the JSON response and check for statistics.
$response = json_decode( $vulnerability, true );
if ( ! isset( $response['stats'] ) ) {
return false;
}
$sponsors = array();
if ( isset( $response['behindtheproject']['sponsors'] ) && count( $response['behindtheproject']['sponsors'] ) ) {
foreach ( $response['behindtheproject']['sponsors'] as $s ) {
$sponsors[] = $s;
unset( $s );
}
}
$contributors = array();
if ( isset( $response['behindtheproject']['contributors'] ) && count( $response['behindtheproject']['contributors'] ) ) {
foreach ( $response['behindtheproject']['contributors'] as $s ) {
$contributors[] = $s;
unset( $s );
}
}
// Return an array with statistical information.
return array(
'core' => array(
'versions' => (int) $response['stats']['products']['core'],
),
'plugins' => array(
'products' => (int) $response['stats']['products']['plugins'],
'vulnerabilities' => (int) $response['stats']['plugins'],
),
'themes' => array(
'products' => (int) $response['stats']['products']['themes'],
'vulnerabilities' => (int) $response['stats']['themes'],
),
'php' => array(
'vulnerabilities' => (int) $response['stats']['php'],
),
'apache' => array(
'vulnerabilities' => (int) $response['stats']['apache'],
),
'nginx' => array(
'vulnerabilities' => (int) $response['stats']['nginx'],
),
'mariadb' => array(
'vulnerabilities' => (int) $response['stats']['mariadb'],
),
'mysql' => array(
'vulnerabilities' => (int) $response['stats']['mysql'],
),
'imagemagick' => array(
'vulnerabilities' => (int) $response['stats']['imagemagick'],
),
'curl' => array(
'vulnerabilities' => (int) $response['stats']['curl'],
),
'memcached' => array(
'vulnerabilities' => (int) $response['stats']['memcached'],
),
'redis' => array(
'vulnerabilities' => (int) $response['stats']['redis'],
),
'sqlite' => array(
'vulnerabilities' => (int) $response['stats']['sqlite'],
),
'sponsors' => $sponsors,
'contributors' => $contributors,
'updated' => array(
'unixepoch' => (int) $response['updated'],
'datetime' => gmdate( 'Y-m-d H:i:s', (int) $response['updated'] ),
'iso8601' => gmdate( 'c', (int) $response['updated'] ),
'rfc2822' => gmdate( 'r', (int) $response['updated'] ),
),
);
}
/**
* Retrieves the latest vulnerability statistics.
*
* This function calls the wpvulnerability API to get fresh statistics related to vulnerabilities
* and returns the updated information.
*
* @since 3.4.0
*
* @return array|false The updated vulnerability statistics, or false on error.
*/
function wpvulnerability_get_fresh_statistics() {
// Call the function to get the latest vulnerability statistics.
$statistics_api_response = wpvulnerability_get_statistics();
// Return the response from the API.
return $statistics_api_response;
}
/**
* Retrieves and caches the latest vulnerability statistics.
*
* This function retrieves the most recent vulnerability statistics, caches the data,
* and returns the information as a JSON-encoded array. The cache expiration timestamp is also updated.
*
* @since 3.4.0
*
* @return string JSON-encoded array containing the vulnerability statistics.
*/
function wpvulnerability_statistics_get() {
// Retrieve fresh statistics.
$statistics = wpvulnerability_get_fresh_statistics();
// Cache the statistics data and the timestamp for cache expiration.
$encoded_statistics = wp_json_encode( $statistics );
$cache_expiration = number_format( time() + ( 3600 * wpvulnerability_cache_hours() ), 0, '.', '' );
if ( is_multisite() ) {
update_site_option( 'wpvulnerability-statistics', $encoded_statistics );
update_site_option( 'wpvulnerability-statistics-cache', $cache_expiration );
} else {
update_option( 'wpvulnerability-statistics', $encoded_statistics );
update_option( 'wpvulnerability-statistics-cache', $cache_expiration );
}
// Return the JSON-encoded array of statistics data.
return $encoded_statistics;
}
/**
* Get vulnerabilities for a specific product version.
*
* This function retrieves vulnerability data for a specified product version.
* It supports caching to minimize API requests and improve performance.
*
* @since 3.5.0
*
* @param string $type The type of product (e.g., 'php', 'apache', 'nginx', 'mariadb', 'mysql').
* @param string $version The version of the product to check.
* @param int $cache Optional. Whether to use cache. Default is 1 (true).
*
* @return array|false Returns an array of vulnerabilities, or false if there are none.
*/
function wpvulnerability_get_vulnerabilities( $type, $version, $cache = 1 ) {
$key = 'wpvulnerability_' . $type;
$vulnerability_data = null;
$vulnerability = array();
// Get cached statistics if available.
if ( $cache ) {
$vulnerability_data = is_multisite() ? get_site_transient( $key ) : get_transient( $key );
}
// If cached statistics are not available, retrieve them from the API and store them in cache.
if ( empty( $vulnerability_data ) ) {
$url = WPVULNERABILITY_API_HOST . $type . '/' . $version . '/';
$response = wp_remote_get( $url, array( 'timeout' => 2500 ) );
if ( ! is_wp_error( $response ) ) {
$body = wp_remote_retrieve_body( $response );
if ( $cache ) {
if ( is_multisite() ) {
set_site_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.
}
}
// If the response does not contain vulnerabilities, return false.
$response = json_decode( $vulnerability_data, true );
if ( ( isset( $response['error'] ) && $response['error'] ) || empty( $response['data']['vulnerability'] ) ) {
return false;
}
// Process each vulnerability.
foreach ( $response['data']['vulnerability'] as $v ) {
// Check if the version falls within the specified min and max operator range.
if ( isset( $v['operator']['min_operator'], $v['operator']['max_operator'] ) &&
! empty( $v['operator']['min_operator'] ) && ! empty( $v['operator']['max_operator'] ) ) {
if ( version_compare( $version, $v['operator']['min_version'], $v['operator']['min_operator'] ) &&
version_compare( $version, $v['operator']['max_version'], $v['operator']['max_operator'] ) ) {
$vulnerability[] = array(
'name' => wp_kses( (string) $v['name'], 'strip' ),
'versions' => wp_kses( wpvulnerability_pretty_operator( $v['operator']['min_operator'] ) . $v['operator']['min_version'] . ' - ' . wpvulnerability_pretty_operator( $v['operator']['max_operator'] ) . $v['operator']['max_version'], 'strip' ),
'version' => wp_kses( (string) $v['operator']['min_version'], 'strip' ),
'unfixed' => (int) $v['operator']['unfixed'],
'source' => $v['source'],
);
}
} elseif ( isset( $v['operator']['max_operator'] ) ) {
if ( version_compare( $version, $v['operator']['max_version'], $v['operator']['max_operator'] ) ) {
$vulnerability[] = array(
'name' => wp_kses( (string) $v['name'], 'strip' ),
'versions' => wp_kses( wpvulnerability_pretty_operator( $v['operator']['max_operator'] ) . $v['operator']['max_version'], 'strip' ),
'version' => wp_kses( (string) $v['operator']['max_version'], 'strip' ),
'unfixed' => (int) $v['operator']['unfixed'],
'source' => $v['source'],
);
}
} elseif ( isset( $v['operator']['min_operator'] ) ) {
if ( version_compare( $version, $v['operator']['min_version'], $v['operator']['min_operator'] ) ) {
$vulnerability[] = array(
'name' => wp_kses( (string) $v['name'], 'strip' ),
'versions' => wp_kses( wpvulnerability_pretty_operator( $v['operator']['min_operator'] ) . $v['operator']['min_version'], 'strip' ),
'version' => wp_kses( (string) $v['operator']['min_version'], 'strip' ),
'unfixed' => (int) $v['operator']['unfixed'],
'source' => $v['source'],
);
}
}
}
return $vulnerability;
}