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;
}
}