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 $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 $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 .= '' . "\n"; $message .= '' . "\n"; $message .= '' . "\n"; $message .= ' ' . "\n"; $message .= ' ' . "\n"; $message .= ' WPVulnerability' . "\n"; $message .= ' ' . "\n"; $message .= '' . "\n"; $message .= '' . "\n"; $message .= ' ' . "\n"; $message .= ' ' . "\n"; $message .= ' ' . "\n"; $message .= ' ' . "\n"; $message .= ' ' . "\n"; $message .= '
' . "\n"; $message .= '
' . "\n"; $message .= ' ' . "\n"; $message .= ' ' . "\n"; $message .= ' ' . "\n"; $message .= ' ' . "\n"; $message .= ' ' . "\n"; $message .= '
' . "\n"; $message .= ' ' . "\n"; $message .= ' ' . "\n"; $message .= ' ' . "\n"; $message .= ' ' . "\n"; $message .= ' ' . "\n"; $message .= ' ' . "\n"; $message .= ' ' . "\n"; $message .= '
' . "\n"; $message .= ' WPVulnerability' . "\n"; $message .= '

' . esc_html( $title ) . '

' . "\n"; // Add the site URL based on the multisite configuration. if ( is_multisite() ) { $message .= '

' . esc_html( network_site_url() ) . '

' . "\n"; } else { $message .= '

' . esc_html( site_url() ) . '

' . "\n"; } $message .= '
' . "\n"; $message .= $content; // Add the main content of the email. $message .= '
' . "\n"; $message .= ' ' . "\n"; $message .= '
' . "\n"; $message .= '
' . "\n"; $message .= '
' . "\n"; $message .= '' . "\n"; $message .= ''; // 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 .= '

' . esc_html__( 'No vulnerabilities found', 'wpvulnerability' ) . '

'; $email_content .= '

' . esc_html__( 'This is likely a test. The site does not have vulnerabilities.', 'wpvulnerability' ) . '

'; } // Append core vulnerabilities HTML to the email content. if ( ! empty( $html_core ) ) { $email_content .= '

' . esc_html__( 'Core vulnerabilities', 'wpvulnerability' ) . '

'; $email_content .= $html_core; } // Append plugins vulnerabilities HTML to the email content. if ( ! empty( $html_plugins ) ) { $email_content .= '

' . esc_html__( 'Plugins vulnerabilities', 'wpvulnerability' ) . '

'; $email_content .= $html_plugins; } // Append themes vulnerabilities HTML to the email content. if ( ! empty( $html_themes ) ) { $email_content .= '

' . esc_html__( 'Themes vulnerabilities', 'wpvulnerability' ) . '

'; $email_content .= $html_themes; } // Append PHP vulnerabilities HTML to the email content. if ( ! empty( $html_php ) ) { $email_content .= '

' . esc_html__( 'PHP vulnerabilities', 'wpvulnerability' ) . '

'; $email_content .= $html_php; } // Append Apache vulnerabilities HTML to the email content. if ( ! empty( $html_apache ) ) { $email_content .= '

' . esc_html__( 'Apache HTTPD vulnerabilities', 'wpvulnerability' ) . '

'; $email_content .= $html_apache; } // Append Nginx vulnerabilities HTML to the email content. if ( ! empty( $html_nginx ) ) { $email_content .= '

' . esc_html__( 'Nginx vulnerabilities', 'wpvulnerability' ) . '

'; $email_content .= $html_nginx; } // Append MariaDB vulnerabilities HTML to the email content. if ( ! empty( $html_mariadb ) ) { $email_content .= '

' . esc_html__( 'MariaDB vulnerabilities', 'wpvulnerability' ) . '

'; $email_content .= $html_mariadb; } // Append MySQL vulnerabilities HTML to the email content. if ( ! empty( $html_mysql ) ) { $email_content .= '

' . esc_html__( 'MySQL vulnerabilities', 'wpvulnerability' ) . '

'; $email_content .= $html_mysql; } // Append ImageMagick vulnerabilities HTML to the email content. if ( ! empty( $html_imagemagick ) ) { $email_content .= '

' . esc_html__( 'ImageMagick vulnerabilities', 'wpvulnerability' ) . '

'; $email_content .= $html_imagemagick; } // Append curl vulnerabilities HTML to the email content. if ( ! empty( $html_curl ) ) { $email_content .= '

' . esc_html__( 'curl vulnerabilities', 'wpvulnerability' ) . '

'; $email_content .= $html_curl; } // Append memcached vulnerabilities HTML to the email content. if ( ! empty( $html_memcached ) ) { $email_content .= '

' . esc_html__( 'memcached vulnerabilities', 'wpvulnerability' ) . '

'; $email_content .= $html_memcached; } // Append Redis vulnerabilities HTML to the email content. if ( ! empty( $html_redis ) ) { $email_content .= '

' . esc_html__( 'Redis vulnerabilities', 'wpvulnerability' ) . '

'; $email_content .= $html_redis; } // Append SQLite vulnerabilities HTML to the email content. if ( ! empty( $html_sqlite ) ) { $email_content .= '

' . esc_html__( 'SQLite vulnerabilities', 'wpvulnerability' ) . '

'; $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 || $forced ) { $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 ) ); // Track whether any webhook channel delivered successfully, so a webhook-only // 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 && wpvulnerability_send_teams_notification( ( is_scalar( $wpvulnerability_settings['teams_webhook'] ) ? (string) $wpvulnerability_settings['teams_webhook'] : '' ), $text_message ) ) { $webhook_sent = true; } if ( $discord_enabled && wpvulnerability_send_discord_notification( ( is_scalar( $wpvulnerability_settings['discord_webhook'] ) ? (string) $wpvulnerability_settings['discord_webhook'] : '' ), $text_message ) ) { $webhook_sent = true; } 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 ) ) { $webhook_sent = true; } return $wpmail || $webhook_sent; } // 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 = '
' . $html . '
'; $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( '' . $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> $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> $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