get( 'use_legacy_blc_version' ) ) ) { return; } /** * Allow the entire highlight feature to be disabled via filter. * Return false to prevent broken links from being highlighted in content. * * @since 2.5.0 * @param bool $enabled Whether highlighting is enabled. Default true. */ if ( ! apply_filters( 'wpmudev_blc_highlight_broken_links', true ) ) { return; } // Only run on the front end, not in the admin. if ( ! is_admin() ) { add_filter( 'the_content', array( $this, 'highlight_broken_links' ), 20 ); add_action( 'wp_head', array( $this, 'output_highlight_styles' ) ); } } /** * Filters post content and adds a CSS class plus data attribute to every * anchor whose href matches a known broken link URL. * * @param string $content The post content. * @return string Modified content. */ public function highlight_broken_links( string $content ) { if ( empty( $content ) ) { return $content; } $broken_urls = $this->get_broken_urls(); if ( empty( $broken_urls ) ) { return $content; } // Fast bail: none of the broken URLs appear anywhere in the content. // Check both the raw URL and its HTML-encoded form (&) because WordPress // serialises ampersands in href attributes as &. $found = false; foreach ( $broken_urls as $url ) { if ( strpos( $content, $url ) !== false || strpos( $content, htmlspecialchars( $url, ENT_QUOTES ) ) !== false ) { $found = true; break; } } if ( ! $found ) { return $content; } $css_class = apply_filters( 'wpmudev_blc_highlight_css_class', 'blc-broken-link' ); $data_attr = apply_filters( 'wpmudev_blc_highlight_data_attr', 'data-blc-broken' ); // Replace & with (?:&|&) so the pattern matches both the raw form // and the HTML-entity form that WordPress writes into href attributes. $broken_urls_escaped = array_map( static function ( $url ) { $url = preg_quote( $url, '/' ); return str_replace( '&', '(?:&|&)', $url ); }, $broken_urls ); $urls_pattern = implode( '|', $broken_urls_escaped ); $modified = false; $pattern = '/(]*href=["\'])(' . $urls_pattern . ')(\/?)(["\'][^>]*>)/i'; $new_content = preg_replace_callback( // Match anchor tags with href attributes containing any of the broken URLs. $pattern, function ( $matches ) use ( $css_class, $data_attr, &$modified ) { $modified = true; // Build the replacement anchor tag with the CSS class and data attribute added. return ''; }, $content ); if ( ! $modified || null === $new_content ) { return $content; } return $new_content; } /** * Outputs minimal inline CSS for broken link highlighting when the page has * any broken links in its content. Falls back gracefully if no links exist. * * Override the appearance via CSS targeting `.blc-broken-link` (default class) * or by filtering `wpmudev_blc_highlight_css_class`. * * @return void */ public function output_highlight_styles() { if ( empty( $this->get_broken_urls() ) ) { return; } // Skip for logged-out users or users whose only capability is 'read' (i.e. subscribers). if ( ! is_user_logged_in() || ! current_user_can( 'edit_posts' ) ) { return; } $css_class = apply_filters( 'wpmudev_blc_highlight_css_class', 'blc-broken-link' ); /** * Filter the inline CSS output for highlighted broken links. * Return an empty string to suppress default styles entirely. * * @since 2.5.0 * @param string $css The default CSS rules. * @param string $css_class The CSS class applied to broken link anchors. */ $css = apply_filters( 'wpmudev_blc_highlight_inline_css', sprintf( '.%s { outline: 2px solid #e53e3e; outline-offset: 1px; }', esc_attr( $css_class ) ), $css_class ); if ( ! empty( $css ) ) { printf( '' . "\n", wp_strip_all_tags( $css ) ); } } /** * Returns a deduplicated, normalised list of broken link URLs from the last * completed scan. Results are cached for the lifetime of the request. * * @return string[] */ private function get_broken_urls(): array { if ( ! is_null( $this->broken_urls ) ) { return $this->broken_urls; } $this->broken_urls = array(); $scan_results = Settings::instance()->get( 'scan_results' ); if ( empty( $scan_results ) || ! is_array( $scan_results ) ) { return $this->broken_urls; } $broken_links_list = $scan_results['broken_links_list'] ?? null; if ( empty( $broken_links_list ) || ! is_array( $broken_links_list ) ) { return $this->broken_urls; } foreach ( $broken_links_list as $broken_link ) { if ( is_object( $broken_link ) ) { $broken_link = (array) $broken_link; } if ( ! empty( $broken_link['is_ignored'] ) ) { continue; } $url = $broken_link['url'] ?? ''; if ( ! empty( $url ) ) { $this->broken_urls[] = untrailingslashit( trim( $url, '\'"' ) ); } } $this->broken_urls = array_unique( $this->broken_urls ); return $this->broken_urls; } }