973 lines
38 KiB
PHP
973 lines
38 KiB
PHP
<?php
|
|
/**
|
|
* Notifications functions
|
|
*
|
|
* @package WPVulnerability
|
|
*
|
|
* @version 2.0.0
|
|
*/
|
|
|
|
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.
|
|
*
|
|
* When the `wpvulnerability_disable_email` query parameter is present and the accompanying
|
|
* nonce validates, this handler disables future email notifications for the current site.
|
|
*
|
|
* @since 4.1.3
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_disable_notifications_via_url() {
|
|
if ( empty( $_GET['wpvulnerability_disable_email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
return;
|
|
}
|
|
|
|
$nonce = isset( $_GET['nonce'] ) ? sanitize_text_field( wp_unslash( is_string( $_GET['nonce'] ) ? $_GET['nonce'] : '' ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
|
|
if ( ! wp_verify_nonce( $nonce, 'wpvulnerability-disable-email' ) ) {
|
|
wp_die( esc_html__( 'Invalid request.', 'wpvulnerability' ) );
|
|
}
|
|
|
|
$settings = is_multisite() ? get_site_option( 'wpvulnerability-config' ) : get_option( 'wpvulnerability-config' );
|
|
|
|
if ( ! is_array( $settings ) ) {
|
|
$settings = array();
|
|
}
|
|
|
|
$notify_settings = isset( $settings['notify'] ) ? $settings['notify'] : array();
|
|
$settings['notify'] = wpvulnerability_normalize_notify_settings( $notify_settings );
|
|
$settings['notify']['email'] = 'n';
|
|
|
|
if ( is_multisite() ) {
|
|
update_site_option( 'wpvulnerability-config', $settings );
|
|
} else {
|
|
update_option( 'wpvulnerability-config', $settings );
|
|
}
|
|
|
|
wp_die(
|
|
esc_html__( 'You have unsubscribed from WPVulnerability notifications.', 'wpvulnerability' ),
|
|
esc_html__( 'WPVulnerability', 'wpvulnerability' ),
|
|
array( 'response' => 200 )
|
|
);
|
|
}
|
|
add_action( 'init', 'wpvulnerability_disable_notifications_via_url' );
|
|
|
|
/**
|
|
* Determine if the webhook host is in the allowed list.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @param string $host Host portion of the webhook URL.
|
|
* @param array<string> $allowed_hosts Allowed host suffixes.
|
|
*
|
|
* @return bool True when the host matches the allow list.
|
|
*/
|
|
function wpvulnerability_is_allowed_webhook_host( $host, $allowed_hosts ) {
|
|
foreach ( $allowed_hosts as $allowed_host ) {
|
|
$allowed_host = strtolower( trim( (string) $allowed_host ) );
|
|
if ( '' === $allowed_host ) {
|
|
continue;
|
|
}
|
|
|
|
if ( $host === $allowed_host ) {
|
|
return true;
|
|
}
|
|
|
|
$suffix = '.' . $allowed_host;
|
|
if ( substr( $host, -strlen( $suffix ) ) === $suffix ) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Validates a webhook URL against an allow list and HTTPS enforcement.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @param string $webhook_url Webhook URL to validate.
|
|
* @param array<string> $allowed_hosts Allowed host suffixes.
|
|
*
|
|
* @return string Sanitized URL or empty string when invalid.
|
|
*/
|
|
function wpvulnerability_validate_webhook_url( $webhook_url, $allowed_hosts ) {
|
|
$webhook_url = esc_url_raw( trim( (string) $webhook_url ) );
|
|
|
|
if ( empty( $webhook_url ) ) {
|
|
return '';
|
|
}
|
|
|
|
$parts = wp_parse_url( $webhook_url );
|
|
|
|
if ( ! is_array( $parts ) || empty( $parts['scheme'] ) || empty( $parts['host'] ) ) {
|
|
return '';
|
|
}
|
|
|
|
if ( 'https' !== strtolower( (string) $parts['scheme'] ) ) {
|
|
return '';
|
|
}
|
|
|
|
$host = strtolower( (string) $parts['host'] );
|
|
|
|
if ( ! wpvulnerability_is_allowed_webhook_host( $host, $allowed_hosts ) ) {
|
|
return '';
|
|
}
|
|
|
|
return $webhook_url;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the unsubscribe URL for WPVulnerability email notifications.
|
|
*
|
|
* Generates a signed URL that disables future email notifications when
|
|
* visited. The URL works in both single and multisite environments and is
|
|
* validated through the `wpvulnerability_disable_notifications_via_url`
|
|
* handler. When running in multisite, it targets the network home only if
|
|
* the plugin is network-activated; otherwise it uses the current site's
|
|
* home URL.
|
|
*
|
|
* @since 4.1.7
|
|
*
|
|
* @return string The unsubscribe URL including the security nonce.
|
|
*/
|
|
function wpvulnerability_get_disable_notifications_url() {
|
|
$nonce = wp_create_nonce( 'wpvulnerability-disable-email' );
|
|
|
|
$base_url = home_url( '/' );
|
|
|
|
if ( is_multisite() ) {
|
|
if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
|
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
|
}
|
|
|
|
$plugin_basename = defined( 'WPVULNERABILITY_PLUGIN_BASE' ) ? WPVULNERABILITY_PLUGIN_BASE : plugin_basename( WPVULNERABILITY_PLUGIN_FILE );
|
|
|
|
if ( function_exists( 'is_plugin_active_for_network' ) && is_plugin_active_for_network( $plugin_basename ) ) {
|
|
$base_url = network_home_url( '/' );
|
|
}
|
|
}
|
|
|
|
return add_query_arg(
|
|
array(
|
|
'wpvulnerability_disable_email' => 1,
|
|
'nonce' => $nonce,
|
|
),
|
|
$base_url
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Prepares the HTML email message.
|
|
*
|
|
* This function generates an HTML email message with the given title and content.
|
|
* It includes basic styling and structure to ensure compatibility with most email clients.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @param string $title The title of the email message.
|
|
* @param string $content The content of the email message.
|
|
*
|
|
* @return string The prepared HTML email message.
|
|
*/
|
|
function wpvulnerability_email_prepare( $title, $content ) {
|
|
|
|
$message = '';
|
|
$message .= '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' . "\n";
|
|
$message .= '<html xmlns="http://www.w3.org/1999/xhtml" style="box-sizing: border-box; margin: 0;">' . "\n";
|
|
$message .= '<head>' . "\n";
|
|
$message .= ' <meta name="viewport" content="width=device-width">' . "\n";
|
|
$message .= ' <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">' . "\n";
|
|
$message .= ' <title>WPVulnerability</title>' . "\n";
|
|
$message .= ' <style type="text/css">' . "\n";
|
|
$message .= ' img { max-width: 100%; }' . "\n";
|
|
$message .= ' body { -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.2em; }' . "\n";
|
|
$message .= ' body { background-color: #f6f6f6; }' . "\n";
|
|
$message .= ' @media only screen and (max-width: 640px) {' . "\n";
|
|
$message .= ' body { padding: 0 !important; }' . "\n";
|
|
$message .= ' h1, h2, h3, h4 { margin: 20px 0 5px 0 !important; }' . "\n";
|
|
$message .= ' .container { padding: 0 !important; width: 100% !important; }' . "\n";
|
|
$message .= ' .content { padding: 0 !important; }' . "\n";
|
|
$message .= ' .content-wrap { padding: 10px !important; }' . "\n";
|
|
$message .= ' .invoice { width: 100% !important; }' . "\n";
|
|
$message .= ' }' . "\n";
|
|
$message .= ' </style>' . "\n";
|
|
$message .= '</head>' . "\n";
|
|
$message .= '<body itemscope itemtype="http://schema.org/EmailMessage" style="box-sizing: border-box; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">' . "\n";
|
|
$message .= ' <table class="body-wrap" style="box-sizing: border-box; width: 100%; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">' . "\n";
|
|
$message .= ' <tr style="box-sizing: border-box; margin: 0;">' . "\n";
|
|
$message .= ' <td style="box-sizing: border-box; vertical-align: top; margin: 0;" valign="top"></td>' . "\n";
|
|
$message .= ' <td class="container" width="600" style="box-sizing: border-box; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;" valign="top">' . "\n";
|
|
$message .= ' <div class="content" style="box-sizing: border-box; max-width: 600px; display: block; margin: 0 auto; padding: 20px;">' . "\n";
|
|
$message .= ' <table class="main" width="100%" cellpadding="0" cellspacing="0" style="box-sizing: border-box; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;" bgcolor="#fff">' . "\n";
|
|
$message .= ' <tr style="box-sizing: border-box; margin: 0;">' . "\n";
|
|
$message .= ' <td class="content-wrap aligncenter" style="box-sizing: border-box; vertical-align: top; text-align: center; margin: 0; padding: 20px;" align="center" valign="top">' . "\n";
|
|
$message .= ' <table width="100%" cellpadding="0" cellspacing="0" style="box-sizing: border-box; margin: 0;">' . "\n";
|
|
$message .= ' <tr style="box-sizing: border-box; margin: 0;">' . "\n";
|
|
$message .= ' <td class="content-block" style="box-sizing: border-box; vertical-align: top; margin: 0; padding: 0 0 20px; text-align: center;" valign="top">' . "\n";
|
|
$message .= ' <img class="aligncenter" src="' . WPVULNERABILITY_PLUGIN_URL . 'assets/icon-128x128.png" width="64" height="64" alt="WPVulnerability">' . "\n";
|
|
$message .= ' <h1 class="aligncenter" style="box-sizing: border-box; color: #000; line-height: 1.2em; text-align: center; margin: 40px 0 0;" align="center">' . esc_html( $title ) . '</h1>' . "\n";
|
|
|
|
// Add the site URL based on the multisite configuration.
|
|
if ( is_multisite() ) {
|
|
$message .= ' <p class="aligncenter" style="box-sizing: border-box; color: #000; line-height: 1.2em; text-align: center; margin: 5px 0 0;" align="center"><a href="' . esc_url( network_site_url() ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( network_site_url() ) . '</a></p>' . "\n";
|
|
} else {
|
|
$message .= ' <p class="aligncenter" style="box-sizing: border-box; color: #000; line-height: 1.2em; text-align: center; margin: 5px 0 0;" align="center"><a href="' . esc_url( site_url() ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( site_url() ) . '</a></p>' . "\n";
|
|
}
|
|
|
|
$message .= ' </td>' . "\n";
|
|
$message .= ' </tr>' . "\n";
|
|
$message .= ' <tr style="box-sizing: border-box; margin: 0;">' . "\n";
|
|
$message .= ' <td class="content-block alignleft" style="box-sizing: border-box; vertical-align: top; text-align: left; margin: 0; padding: 0 0 20px;" valign="top">' . "\n";
|
|
$message .= $content; // Add the main content of the email.
|
|
$message .= ' </td>' . "\n";
|
|
$message .= ' </tr>' . "\n";
|
|
$message .= ' </table>' . "\n";
|
|
$message .= ' <div class="footer" style="box-sizing: border-box; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">' . "\n";
|
|
$message .= ' <table width="100%" style="box-sizing: border-box; margin: 0;">' . "\n";
|
|
$message .= ' <tr style="box-sizing: border-box; margin: 0;">' . "\n";
|
|
$message .= ' <td class="aligncenter content-block" style="box-sizing: border-box; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top">' . "\n";
|
|
$message .= sprintf(
|
|
// translators: %1$s the website of Database, %2$s database site name.
|
|
__( 'Learn more about the WordPress Vulnerability Database API at <a href="%1$s">%2$s</a>', 'wpvulnerability' ),
|
|
'https://www.wpvulnerability.com/',
|
|
'WPVulnerability'
|
|
);
|
|
$message .= ' </td>' . "\n";
|
|
$message .= ' </tr>' . "\n";
|
|
|
|
$unsubscribe_url = wpvulnerability_get_disable_notifications_url();
|
|
$unsubscribe = wp_kses(
|
|
sprintf(
|
|
// translators: %1$s unsubscribe URL.
|
|
__( 'Want to stop receiving these alerts? <a href="%1$s">Unsubscribe</a> or change the email frequency to <strong>Never</strong> in the WPVulnerability settings.', 'wpvulnerability' ),
|
|
esc_url( $unsubscribe_url )
|
|
),
|
|
array(
|
|
'a' => array(
|
|
'href' => array(),
|
|
),
|
|
'strong' => array(),
|
|
)
|
|
);
|
|
|
|
$message .= ' <tr style="box-sizing: border-box; margin: 0;">' . "\n";
|
|
$message .= ' <td class="aligncenter content-block" style="box-sizing: border-box; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top">' . "\n";
|
|
$message .= ' <p style="box-sizing: border-box; color: #999; text-align: center; margin: 0;">' . $unsubscribe . '</p>' . "\n";
|
|
$message .= ' </td>' . "\n";
|
|
$message .= ' </tr>' . "\n";
|
|
|
|
// Add the site URL in the footer based on the multisite configuration.
|
|
if ( is_multisite() ) {
|
|
$message .= ' <tr style="box-sizing: border-box; margin: 0;">' . "\n";
|
|
$message .= ' <td class="aligncenter content-block" style="box-sizing: border-box; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top"><a href="' . esc_url( network_site_url() ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( network_site_url() ) . '</a></td>' . "\n";
|
|
$message .= ' </tr>' . "\n";
|
|
} else {
|
|
$message .= ' <tr style="box-sizing: border-box; margin: 0;">' . "\n";
|
|
$message .= ' <td class="aligncenter content-block" style="box-sizing: border-box; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top"><a href="' . esc_url( site_url() ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( site_url() ) . '</a></td>' . "\n";
|
|
$message .= ' </tr>' . "\n";
|
|
}
|
|
|
|
$message .= ' </table>' . "\n";
|
|
$message .= ' </div>' . "\n";
|
|
$message .= ' </td>' . "\n";
|
|
$message .= ' <td style="box-sizing: border-box; vertical-align: top; margin: 0;" valign="top"></td>' . "\n";
|
|
$message .= ' </tr>' . "\n";
|
|
$message .= ' </table>' . "\n";
|
|
$message .= ' </div>' . "\n";
|
|
$message .= ' </td>' . "\n";
|
|
$message .= ' </tr>' . "\n";
|
|
$message .= ' </table>' . "\n";
|
|
$message .= '</body>' . "\n";
|
|
$message .= '</html>';
|
|
|
|
// Return the prepared HTML email message.
|
|
return $message;
|
|
}
|
|
|
|
/**
|
|
* Send a vulnerability notification to Slack.
|
|
*
|
|
* @since 4.1.3
|
|
*
|
|
* @param string $webhook_url Slack webhook URL.
|
|
* @param string $message Message body to deliver.
|
|
*
|
|
* @return bool True on success, false on failure.
|
|
*/
|
|
function wpvulnerability_send_slack_notification( $webhook_url, $message ) {
|
|
$webhook_url = wpvulnerability_validate_webhook_url(
|
|
$webhook_url,
|
|
array(
|
|
'hooks.slack.com',
|
|
)
|
|
);
|
|
|
|
if ( empty( $webhook_url ) || empty( $message ) ) {
|
|
return false;
|
|
}
|
|
|
|
$encoded_slack = wp_json_encode( array( 'text' => $message ) );
|
|
$args = array(
|
|
'body' => false !== $encoded_slack ? $encoded_slack : '',
|
|
'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ),
|
|
'timeout' => 10,
|
|
'data_format' => 'body',
|
|
);
|
|
|
|
$response = wp_remote_post( $webhook_url, $args );
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
return false;
|
|
}
|
|
|
|
$status_code = (int) wp_remote_retrieve_response_code( $response );
|
|
|
|
return $status_code >= 200 && $status_code < 300;
|
|
}
|
|
|
|
/**
|
|
* Send a vulnerability notification to Microsoft Teams.
|
|
*
|
|
* @since 4.1.3
|
|
*
|
|
* @param string $webhook_url Teams webhook URL.
|
|
* @param string $message Message body to deliver.
|
|
*
|
|
* @return bool True on success, false on failure.
|
|
*/
|
|
function wpvulnerability_send_teams_notification( $webhook_url, $message ) {
|
|
$webhook_url = wpvulnerability_validate_webhook_url(
|
|
$webhook_url,
|
|
array(
|
|
'office.com',
|
|
'office365.com',
|
|
'api.hooks.microsoft.com',
|
|
)
|
|
);
|
|
|
|
if ( empty( $webhook_url ) || empty( $message ) ) {
|
|
return false;
|
|
}
|
|
|
|
$encoded_teams = wp_json_encode( array( 'text' => $message ) );
|
|
$args = array(
|
|
'body' => false !== $encoded_teams ? $encoded_teams : '',
|
|
'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ),
|
|
'timeout' => 10,
|
|
'data_format' => 'body',
|
|
);
|
|
|
|
$response = wp_remote_post( $webhook_url, $args );
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
return false;
|
|
}
|
|
|
|
$status_code = (int) wp_remote_retrieve_response_code( $response );
|
|
|
|
return $status_code >= 200 && $status_code < 300;
|
|
}
|
|
|
|
/**
|
|
* Send a vulnerability notification to Discord.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @param string $webhook_url Discord webhook URL.
|
|
* @param string $message Message body to deliver.
|
|
*
|
|
* @return bool True on success, false on failure.
|
|
*/
|
|
function wpvulnerability_send_discord_notification( $webhook_url, $message ) {
|
|
$webhook_url = wpvulnerability_validate_webhook_url(
|
|
$webhook_url,
|
|
array(
|
|
'discord.com',
|
|
'discordapp.com',
|
|
)
|
|
);
|
|
|
|
if ( empty( $webhook_url ) || empty( $message ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Discord has a 2000 character limit per message.
|
|
if ( strlen( $message ) > 2000 ) {
|
|
$message = substr( $message, 0, 1997 ) . '...';
|
|
}
|
|
|
|
$encoded_discord = wp_json_encode( array( 'content' => $message ) );
|
|
$args = array(
|
|
'body' => false !== $encoded_discord ? $encoded_discord : '',
|
|
'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ),
|
|
'timeout' => 10,
|
|
'data_format' => 'body',
|
|
);
|
|
|
|
$response = wp_remote_post( $webhook_url, $args );
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
return false;
|
|
}
|
|
|
|
$status_code = (int) wp_remote_retrieve_response_code( $response );
|
|
|
|
return $status_code >= 200 && $status_code < 300;
|
|
}
|
|
|
|
/**
|
|
* Send a vulnerability notification to Telegram.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @param string $bot_token Telegram bot token.
|
|
* @param string $chat_id Telegram chat ID.
|
|
* @param string $message Message body to deliver.
|
|
*
|
|
* @return bool True on success, false on failure.
|
|
*/
|
|
function wpvulnerability_send_telegram_notification( $bot_token, $chat_id, $message ) {
|
|
// Sanitize inputs.
|
|
$bot_token = sanitize_text_field( trim( (string) $bot_token ) );
|
|
$chat_id = sanitize_text_field( trim( (string) $chat_id ) );
|
|
|
|
if ( empty( $bot_token ) || empty( $chat_id ) || empty( $message ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Validate bot token format (should be like: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11).
|
|
if ( ! preg_match( '/^\d+:[A-Za-z0-9_-]+$/', $bot_token ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Telegram has a 4096 character limit per message.
|
|
if ( strlen( $message ) > 4096 ) {
|
|
$message = substr( $message, 0, 4093 ) . '...';
|
|
}
|
|
|
|
$api_url = 'https://api.telegram.org/bot' . $bot_token . '/sendMessage';
|
|
|
|
$encoded_telegram = wp_json_encode(
|
|
array(
|
|
'chat_id' => $chat_id,
|
|
'text' => $message,
|
|
)
|
|
);
|
|
$args = array(
|
|
'body' => false !== $encoded_telegram ? $encoded_telegram : '',
|
|
'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ),
|
|
'timeout' => 10,
|
|
'data_format' => 'body',
|
|
);
|
|
|
|
$response = wp_remote_post( $api_url, $args );
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
return false;
|
|
}
|
|
|
|
$status_code = (int) wp_remote_retrieve_response_code( $response );
|
|
|
|
return $status_code >= 200 && $status_code < 300;
|
|
}
|
|
|
|
/**
|
|
* Executes the vulnerability notification process for a WordPress site.
|
|
*
|
|
* This function checks for vulnerabilities in the WordPress core, plugins, themes, PHP environment, and web server components.
|
|
* It generates an HTML email report detailing any vulnerabilities found. If the function is called with
|
|
* the $forced parameter set to true, it will send an email even if no vulnerabilities are found, which is useful for testing purposes.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @param bool $forced Optional. If set to true, forces the sending of a notification email regardless of whether vulnerabilities are found. Default false.
|
|
* @return bool True when the email was successfully sent, false otherwise.
|
|
*/
|
|
function wpvulnerability_execute_notification( $forced = false ) {
|
|
$email_content = '';
|
|
$wpvulnerability_settings = is_multisite() ? get_site_option( 'wpvulnerability-config' ) : get_option( 'wpvulnerability-config' );
|
|
|
|
if ( ! is_array( $wpvulnerability_settings ) ) {
|
|
$wpvulnerability_settings = array();
|
|
}
|
|
|
|
$notify_settings = isset( $wpvulnerability_settings['notify'] ) ? $wpvulnerability_settings['notify'] : array();
|
|
$wpvulnerability_settings['notify'] = wpvulnerability_normalize_notify_settings( $notify_settings );
|
|
|
|
$email_enabled = wpvulnerability_is_yes( $wpvulnerability_settings['notify']['email'] ) && ! empty( $wpvulnerability_settings['emails'] );
|
|
$slack_enabled = wpvulnerability_is_yes( $wpvulnerability_settings['notify']['slack'] ) && ! empty( $wpvulnerability_settings['slack_webhook'] );
|
|
$teams_enabled = wpvulnerability_is_yes( $wpvulnerability_settings['notify']['teams'] ) && ! empty( $wpvulnerability_settings['teams_webhook'] );
|
|
$discord_enabled = wpvulnerability_is_yes( $wpvulnerability_settings['notify']['discord'] ) && ! empty( $wpvulnerability_settings['discord_webhook'] );
|
|
$telegram_enabled = wpvulnerability_is_yes( $wpvulnerability_settings['notify']['telegram'] ) && ! empty( $wpvulnerability_settings['telegram_bot_token'] ) && ! empty( $wpvulnerability_settings['telegram_chat_id'] );
|
|
|
|
if ( ! $forced && ( empty( $wpvulnerability_settings['period'] ) || ( ! $email_enabled && ! $slack_enabled && ! $teams_enabled && ! $discord_enabled && ! $telegram_enabled ) ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Generate HTML for core, plugins, and themes vulnerabilities.
|
|
$html_core = wpvulnerability_analyze_filter( 'core' ) && wpvulnerability_get_component_count( 'core' ) ? wpvulnerability_html_core() : null;
|
|
|
|
$html_plugins = wpvulnerability_analyze_filter( 'plugins' ) && wpvulnerability_get_component_count( 'plugins' ) ? wpvulnerability_html_plugins() : null;
|
|
|
|
$html_themes = wpvulnerability_analyze_filter( 'themes' ) && wpvulnerability_get_component_count( 'themes' ) ? wpvulnerability_html_themes() : null;
|
|
|
|
// Generate HTML for PHP, Apache, Nginx, MariaDB, MySQL... vulnerabilities.
|
|
$html_php = wpvulnerability_analyze_filter( 'php' ) && wpvulnerability_get_component_count( 'php' ) ? wpvulnerability_html_software( 'php' ) : null;
|
|
|
|
$html_apache = wpvulnerability_analyze_filter( 'apache' ) && wpvulnerability_get_component_count( 'apache' ) ? wpvulnerability_html_software( 'apache' ) : null;
|
|
|
|
$html_nginx = wpvulnerability_analyze_filter( 'nginx' ) && wpvulnerability_get_component_count( 'nginx' ) ? wpvulnerability_html_software( 'nginx' ) : null;
|
|
|
|
$html_mariadb = wpvulnerability_analyze_filter( 'mariadb' ) && wpvulnerability_get_component_count( 'mariadb' ) ? wpvulnerability_html_software( 'mariadb' ) : null;
|
|
|
|
$html_mysql = wpvulnerability_analyze_filter( 'mysql' ) && wpvulnerability_get_component_count( 'mysql' ) ? wpvulnerability_html_software( 'mysql' ) : null;
|
|
|
|
$html_imagemagick = wpvulnerability_analyze_filter( 'imagemagick' ) && wpvulnerability_get_component_count( 'imagemagick' ) ? wpvulnerability_html_software( 'imagemagick' ) : null;
|
|
|
|
$html_curl = wpvulnerability_analyze_filter( 'curl' ) && wpvulnerability_get_component_count( 'curl' ) ? wpvulnerability_html_software( 'curl' ) : null;
|
|
|
|
$html_memcached = wpvulnerability_analyze_filter( 'memcached' ) && wpvulnerability_get_component_count( 'memcached' ) ? wpvulnerability_html_software( 'memcached' ) : null;
|
|
|
|
$html_redis = wpvulnerability_analyze_filter( 'redis' ) && wpvulnerability_get_component_count( 'redis' ) ? wpvulnerability_html_software( 'redis' ) : null;
|
|
|
|
$html_sqlite = wpvulnerability_analyze_filter( 'sqlite' ) && wpvulnerability_get_component_count( 'sqlite' ) ? wpvulnerability_html_software( 'sqlite' ) : null;
|
|
|
|
$all_empty = ( empty( $html_core ) && empty( $html_plugins ) && empty( $html_themes ) && empty( $html_php ) && empty( $html_apache ) && empty( $html_nginx ) && empty( $html_mariadb ) && empty( $html_mysql ) && empty( $html_imagemagick ) && empty( $html_curl ) && empty( $html_memcached ) && empty( $html_redis ) && empty( $html_sqlite ) );
|
|
|
|
// If forced email sending is not enabled and no vulnerabilities were found, exit the function.
|
|
if ( ! $forced && $all_empty ) {
|
|
return false;
|
|
} elseif ( $forced && $all_empty ) {
|
|
$email_content .= '<h2>' . esc_html__( 'No vulnerabilities found', 'wpvulnerability' ) . '</h2>';
|
|
$email_content .= '<p>' . esc_html__( 'This is likely a test. The site does not have vulnerabilities.', 'wpvulnerability' ) . '</p>';
|
|
}
|
|
|
|
// Append core vulnerabilities HTML to the email content.
|
|
if ( ! empty( $html_core ) ) {
|
|
$email_content .= '<h2>' . esc_html__( 'Core vulnerabilities', 'wpvulnerability' ) . '</h2>';
|
|
$email_content .= $html_core;
|
|
}
|
|
|
|
// Append plugins vulnerabilities HTML to the email content.
|
|
if ( ! empty( $html_plugins ) ) {
|
|
$email_content .= '<h2>' . esc_html__( 'Plugins vulnerabilities', 'wpvulnerability' ) . '</h2>';
|
|
$email_content .= $html_plugins;
|
|
}
|
|
|
|
// Append themes vulnerabilities HTML to the email content.
|
|
if ( ! empty( $html_themes ) ) {
|
|
$email_content .= '<h2>' . esc_html__( 'Themes vulnerabilities', 'wpvulnerability' ) . '</h2>';
|
|
$email_content .= $html_themes;
|
|
}
|
|
|
|
// Append PHP vulnerabilities HTML to the email content.
|
|
if ( ! empty( $html_php ) ) {
|
|
$email_content .= '<h2>' . esc_html__( 'PHP vulnerabilities', 'wpvulnerability' ) . '</h2>';
|
|
$email_content .= $html_php;
|
|
}
|
|
|
|
// Append Apache vulnerabilities HTML to the email content.
|
|
if ( ! empty( $html_apache ) ) {
|
|
$email_content .= '<h2>' . esc_html__( 'Apache HTTPD vulnerabilities', 'wpvulnerability' ) . '</h2>';
|
|
$email_content .= $html_apache;
|
|
}
|
|
|
|
// Append Nginx vulnerabilities HTML to the email content.
|
|
if ( ! empty( $html_nginx ) ) {
|
|
$email_content .= '<h2>' . esc_html__( 'Nginx vulnerabilities', 'wpvulnerability' ) . '</h2>';
|
|
$email_content .= $html_nginx;
|
|
}
|
|
|
|
// Append MariaDB vulnerabilities HTML to the email content.
|
|
if ( ! empty( $html_mariadb ) ) {
|
|
$email_content .= '<h2>' . esc_html__( 'MariaDB vulnerabilities', 'wpvulnerability' ) . '</h2>';
|
|
$email_content .= $html_mariadb;
|
|
}
|
|
|
|
// Append MySQL vulnerabilities HTML to the email content.
|
|
if ( ! empty( $html_mysql ) ) {
|
|
$email_content .= '<h2>' . esc_html__( 'MySQL vulnerabilities', 'wpvulnerability' ) . '</h2>';
|
|
$email_content .= $html_mysql;
|
|
}
|
|
|
|
// Append ImageMagick vulnerabilities HTML to the email content.
|
|
if ( ! empty( $html_imagemagick ) ) {
|
|
$email_content .= '<h2>' . esc_html__( 'ImageMagick vulnerabilities', 'wpvulnerability' ) . '</h2>';
|
|
$email_content .= $html_imagemagick;
|
|
}
|
|
|
|
// Append curl vulnerabilities HTML to the email content.
|
|
if ( ! empty( $html_curl ) ) {
|
|
$email_content .= '<h2>' . esc_html__( 'curl vulnerabilities', 'wpvulnerability' ) . '</h2>';
|
|
$email_content .= $html_curl;
|
|
}
|
|
|
|
// Append memcached vulnerabilities HTML to the email content.
|
|
if ( ! empty( $html_memcached ) ) {
|
|
$email_content .= '<h2>' . esc_html__( 'memcached vulnerabilities', 'wpvulnerability' ) . '</h2>';
|
|
$email_content .= $html_memcached;
|
|
}
|
|
|
|
// Append Redis vulnerabilities HTML to the email content.
|
|
if ( ! empty( $html_redis ) ) {
|
|
$email_content .= '<h2>' . esc_html__( 'Redis vulnerabilities', 'wpvulnerability' ) . '</h2>';
|
|
$email_content .= $html_redis;
|
|
}
|
|
|
|
// Append SQLite vulnerabilities HTML to the email content.
|
|
if ( ! empty( $html_sqlite ) ) {
|
|
$email_content .= '<h2>' . esc_html__( 'SQLite vulnerabilities', 'wpvulnerability' ) . '</h2>';
|
|
$email_content .= $html_sqlite;
|
|
}
|
|
|
|
// Get the site name.
|
|
$admin_site = is_multisite() ? get_site_option( 'site_name' ) : get_bloginfo( 'name' );
|
|
|
|
// Get the admin email.
|
|
$admin_email = is_multisite() ? get_site_option( 'admin_email' ) : get_bloginfo( 'admin_email' );
|
|
$from_email = $admin_email;
|
|
|
|
// Check if WPVULNERABILITY_MAIL is defined and valid, and use it if available.
|
|
if ( defined( 'WPVULNERABILITY_MAIL' ) ) {
|
|
$wpvulnerability_sender_email = sanitize_email( trim( (string) WPVULNERABILITY_MAIL ) );
|
|
if ( is_email( $wpvulnerability_sender_email ) ) {
|
|
$from_email = $wpvulnerability_sender_email;
|
|
}
|
|
unset( $wpvulnerability_sender_email );
|
|
}
|
|
|
|
// Prepare email subject and content.
|
|
$email_subject = sprintf(
|
|
// translators: Site name.
|
|
__( 'Vulnerability found: %s', 'wpvulnerability' ),
|
|
( is_scalar( $admin_site ) ? (string) $admin_site : '' )
|
|
);
|
|
|
|
$email_prepared = wpvulnerability_email_prepare( esc_html__( 'Vulnerability found', 'wpvulnerability' ), $email_content );
|
|
|
|
// Prepare email headers.
|
|
$email_headers = array();
|
|
$email_headers[] = 'From: WPVulnerability <' . ( is_scalar( $from_email ) ? (string) $from_email : '' ) . '>';
|
|
$email_headers[] = 'Content-Type: text/html; charset=UTF-8';
|
|
|
|
if ( $forced && ( empty( $wpvulnerability_settings['emails'] ) ) ) {
|
|
// Determine the recipient email.
|
|
$wpvulnerability_settings['emails'] = array( $admin_email );
|
|
}
|
|
|
|
$wpmail = false;
|
|
|
|
if ( $email_enabled ) {
|
|
$mail_to = is_array( $wpvulnerability_settings['emails'] ) ? array_map(
|
|
static function ( $e ) {
|
|
return is_scalar( $e ) ? (string) $e : '';
|
|
},
|
|
$wpvulnerability_settings['emails']
|
|
) : ( is_scalar( $wpvulnerability_settings['emails'] ) ? (string) $wpvulnerability_settings['emails'] : '' );
|
|
$wpmail = wp_mail( $mail_to, $email_subject, $email_prepared, $email_headers );
|
|
}
|
|
|
|
$text_message_body = wpvulnerability_html_to_plain_text( $email_content );
|
|
$text_message = trim( (string) ( $email_subject . "\n\n" . $text_message_body ) );
|
|
|
|
if ( $slack_enabled ) {
|
|
wpvulnerability_send_slack_notification( ( is_scalar( $wpvulnerability_settings['slack_webhook'] ) ? (string) $wpvulnerability_settings['slack_webhook'] : '' ), $text_message );
|
|
}
|
|
|
|
if ( $teams_enabled ) {
|
|
wpvulnerability_send_teams_notification( ( is_scalar( $wpvulnerability_settings['teams_webhook'] ) ? (string) $wpvulnerability_settings['teams_webhook'] : '' ), $text_message );
|
|
}
|
|
|
|
if ( $discord_enabled ) {
|
|
wpvulnerability_send_discord_notification( ( is_scalar( $wpvulnerability_settings['discord_webhook'] ) ? (string) $wpvulnerability_settings['discord_webhook'] : '' ), $text_message );
|
|
}
|
|
|
|
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 );
|
|
}
|
|
|
|
return $wpmail;
|
|
}
|
|
|
|
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOM properties follow upstream naming.
|
|
/**
|
|
* Convert HTML notification content into a plain text representation.
|
|
*
|
|
* Ensures notifications sent to chat platforms retain meaningful structure by
|
|
* translating headings, paragraphs, lists, tables, and links into readable
|
|
* plain text. List indentation and table rows are preserved with appropriate
|
|
* line breaks so the resulting message can be consumed without HTML support.
|
|
*
|
|
* @since 4.1.4
|
|
*
|
|
* @param string $html HTML fragment to convert.
|
|
*
|
|
* @return string Normalized plain text message.
|
|
*/
|
|
function wpvulnerability_html_to_plain_text( $html ) {
|
|
$html = (string) $html;
|
|
|
|
if ( '' === trim( (string) $html ) ) {
|
|
return '';
|
|
}
|
|
|
|
if ( ! class_exists( 'DOMDocument', false ) ) {
|
|
return wp_strip_all_tags( $html );
|
|
}
|
|
|
|
$libxml_previous_state = libxml_use_internal_errors( true );
|
|
$dom = new DOMDocument();
|
|
$wrapped_html = '<div>' . $html . '</div>';
|
|
$load_flags = 0;
|
|
|
|
if ( defined( 'LIBXML_HTML_NOIMPLIED' ) ) {
|
|
$load_flags |= LIBXML_HTML_NOIMPLIED;
|
|
}
|
|
|
|
if ( defined( 'LIBXML_HTML_NODEFDTD' ) ) {
|
|
$load_flags |= LIBXML_HTML_NODEFDTD;
|
|
}
|
|
|
|
$dom->loadHTML( '<?xml encoding="utf-8" ?>' . $wrapped_html, $load_flags );
|
|
libxml_clear_errors();
|
|
libxml_use_internal_errors( $libxml_previous_state );
|
|
|
|
$list_stack = array();
|
|
$output = '';
|
|
|
|
$doc_element = $dom->documentElement;
|
|
if ( null !== $doc_element ) {
|
|
foreach ( $doc_element->childNodes as $child_node ) {
|
|
$output .= wpvulnerability_dom_node_to_plain_text( $child_node, $list_stack );
|
|
}
|
|
}
|
|
|
|
$output = html_entity_decode( $output, ENT_QUOTES, 'UTF-8' );
|
|
$output = preg_replace( '#/\\*.*?\\*/#s', '', $output ) ?? $output;
|
|
$output = preg_replace( '/[ \t]+\n/', "\n", $output ) ?? $output;
|
|
$output = preg_replace( "/\n{3,}/", "\n\n", $output ) ?? $output;
|
|
|
|
return trim( (string) $output );
|
|
}
|
|
|
|
/**
|
|
* Recursively convert DOM nodes to plain text.
|
|
*
|
|
* @since 4.1.4
|
|
*
|
|
* @param DOMNode $node Node being transformed.
|
|
* @param array<int, array<string, mixed>> $list_stack Stack describing parent list context.
|
|
*
|
|
* @return string Plain text representation of the node.
|
|
*/
|
|
function wpvulnerability_dom_node_to_plain_text( DOMNode $node, array &$list_stack ): string {
|
|
if ( XML_TEXT_NODE === $node->nodeType ) {
|
|
$raw_value = $node->nodeValue ?? '';
|
|
return preg_replace( '/\\s+/u', ' ', $raw_value ) ?? $raw_value;
|
|
}
|
|
|
|
if ( XML_ELEMENT_NODE !== $node->nodeType ) {
|
|
return '';
|
|
}
|
|
|
|
$tag_name = strtolower( (string) $node->nodeName );
|
|
|
|
switch ( $tag_name ) {
|
|
case 'br':
|
|
return "\n";
|
|
|
|
case 'p':
|
|
case 'div':
|
|
case 'section':
|
|
case 'article':
|
|
case 'header':
|
|
case 'footer':
|
|
$content = trim( (string) wpvulnerability_dom_children_to_plain_text( $node, $list_stack ) );
|
|
|
|
return '' === $content ? '' : $content . "\n\n";
|
|
|
|
case 'h1':
|
|
case 'h2':
|
|
case 'h3':
|
|
case 'h4':
|
|
case 'h5':
|
|
case 'h6':
|
|
$content = trim( (string) wpvulnerability_dom_children_to_plain_text( $node, $list_stack ) );
|
|
|
|
return '' === $content ? '' : $content . "\n\n";
|
|
|
|
case 'strong':
|
|
case 'em':
|
|
case 'span':
|
|
case 'code':
|
|
case 'b':
|
|
case 'i':
|
|
case 'u':
|
|
case 'small':
|
|
return wpvulnerability_dom_children_to_plain_text( $node, $list_stack );
|
|
|
|
case 'a':
|
|
$content = trim( (string) wpvulnerability_dom_children_to_plain_text( $node, $list_stack ) );
|
|
$href = '';
|
|
|
|
if ( null !== $node->attributes && null !== $node->attributes->getNamedItem( 'href' ) ) {
|
|
$href_attribute = $node->attributes->getNamedItem( 'href' );
|
|
$href = trim( (string) $href_attribute->nodeValue );
|
|
}
|
|
|
|
if ( '' !== $href && '' !== $content && false === strpos( $content, $href ) ) {
|
|
return $content . ' (' . $href . ')';
|
|
}
|
|
|
|
return $content;
|
|
|
|
case 'ul':
|
|
case 'ol':
|
|
$list_stack[] = array(
|
|
'type' => $tag_name,
|
|
'index' => 0,
|
|
);
|
|
$content = wpvulnerability_dom_children_to_plain_text( $node, $list_stack );
|
|
array_pop( $list_stack );
|
|
|
|
return $content . ( '' === $content ? '' : "\n" );
|
|
|
|
case 'li':
|
|
$depth = count( $list_stack );
|
|
$indent = $depth > 0 ? str_repeat( ' ', $depth - 1 ) : '';
|
|
$marker = '- ';
|
|
|
|
if ( $depth > 0 ) {
|
|
$current_index = $depth - 1;
|
|
$list_stack[ $current_index ]['index'] = ( is_int( $list_stack[ $current_index ]['index'] ) ? $list_stack[ $current_index ]['index'] : 0 ) + 1;
|
|
|
|
if ( isset( $list_stack[ $current_index ]['type'] ) && 'ol' === $list_stack[ $current_index ]['type'] ) {
|
|
$marker = $list_stack[ $current_index ]['index'] . '. ';
|
|
}
|
|
}
|
|
|
|
$content = trim( (string) wpvulnerability_dom_children_to_plain_text( $node, $list_stack ) );
|
|
|
|
if ( '' === $content ) {
|
|
return '';
|
|
}
|
|
|
|
$content = preg_replace( '/\n/', "\n" . $indent . ' ', $content ) ?? $content;
|
|
|
|
return $indent . $marker . $content . "\n";
|
|
|
|
case 'table':
|
|
$rows = trim( (string) wpvulnerability_dom_children_to_plain_text( $node, $list_stack ) );
|
|
|
|
return '' === $rows ? '' : $rows . "\n";
|
|
|
|
case 'thead':
|
|
case 'tbody':
|
|
case 'tfoot':
|
|
case 'tr':
|
|
return wpvulnerability_dom_children_to_plain_text( $node, $list_stack );
|
|
|
|
case 'th':
|
|
case 'td':
|
|
$content = trim( (string) wpvulnerability_dom_children_to_plain_text( $node, $list_stack ) );
|
|
|
|
return '' === $content ? '' : $content . ' | ';
|
|
|
|
default:
|
|
return wpvulnerability_dom_children_to_plain_text( $node, $list_stack );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate plain text for the children of a DOM node.
|
|
*
|
|
* @since 4.1.4
|
|
*
|
|
* @param DOMNode $node Parent DOM node.
|
|
* @param array<int, array<string, mixed>> $list_stack Stack describing parent list context.
|
|
*
|
|
* @return string Concatenated plain text for child nodes.
|
|
*/
|
|
function wpvulnerability_dom_children_to_plain_text( DOMNode $node, array &$list_stack ): string {
|
|
$text = '';
|
|
|
|
foreach ( $node->childNodes as $child ) {
|
|
$child_text = wpvulnerability_dom_node_to_plain_text( $child, $list_stack );
|
|
|
|
if ( '' === $child_text ) {
|
|
continue;
|
|
}
|
|
|
|
$text .= $child_text;
|
|
}
|
|
|
|
if ( 'td' === strtolower( (string) $node->nodeName ) || 'th' === strtolower( (string) $node->nodeName ) ) {
|
|
$text = rtrim( (string) $text, ' |' );
|
|
}
|
|
|
|
if ( 'tr' === strtolower( (string) $node->nodeName ) ) {
|
|
$text = rtrim( (string) $text, ' |' );
|
|
$text = '' === $text ? '' : $text . "\n";
|
|
}
|
|
|
|
return $text;
|
|
}
|
|
// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|