File "image-seo.php"
Full Path: /home/fresvfqn/waterdamagerestorationandrepairsmithtown.com/wp-content/plugins/surerank/inc/frontend/image-seo.php
File size: 17.32 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* Image SEO Enhancement Module
*
* Automatically enhances images with missing accessibility attributes.
*
* @package surerank
* @since 1.4.0
*/
namespace SureRank\Inc\Frontend;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use SureRank\Inc\Traits\Get_Instance;
/**
* Image SEO enhancement handler
*
* @since 1.4.0
*/
class Image_Seo {
use Get_Instance;
/**
* Initialize image enhancement
*
* @since 1.4.0
*/
public function __construct() {
if ( $this->status() ) {
$this->register_enhancement_hooks();
}
}
/**
* Check if image enhancement is active
*
* @return bool
* @since 1.4.0
*/
public function status(): bool {
return apply_filters( 'surerank_auto_set_image_title_and_alt', true );
}
/**
* Enhance content with missing image attributes
*
* @param string $content Content to enhance.
* @param int|null $post_id Post ID context.
* @return string Enhanced content
* @since 1.4.0
*/
public function enhance_content( $content, $post_id = null ) {
if ( empty( $content ) || strpos( $content, '<img' ) === false ) {
return $content;
}
return $this->process_content_images( $content, $post_id );
}
/**
* Register WordPress content enhancement hooks
*
* @since 1.4.0
* @return void
*/
private function register_enhancement_hooks(): void {
$filters = [
'the_content' => 11,
'post_thumbnail_html' => 11,
'woocommerce_single_product_image_thumbnail_html' => 11,
];
foreach ( $filters as $hook => $priority ) {
add_filter( $hook, [ $this, 'enhance_content' ], $priority, 2 );
}
}
/**
* Process all images in content
*
* @param string $content Content with images.
* @param int|null $post_id Post context.
* @return string Processed content
* @since 1.4.0
*/
private function process_content_images( $content, $post_id ): string {
$clean_content = $this->remove_script_style_tags( $content );
$image_tags = $this->extract_image_tags( $clean_content );
if ( empty( $image_tags ) ) {
return $content;
}
$context = $this->build_processing_context( $post_id );
return $this->enhance_image_tags( $content, $image_tags, $context );
}
/**
* Remove script and style tags from content
*
* @param string $content Raw content.
* @return string Cleaned content
* @since 1.4.0
*/
private function remove_script_style_tags( $content ): string {
/**
* Using regex to remove script and style tags
*
* Regex pattern breakdown:
*
* < matches the start of a tag
* (script|style) matches either script or style
* [^>]*? matches any character except > (using lazy quantifier)
* .*? matches any character any number of times (using lazy quantifier)
* <\/\1> matches the closing tag of the same type as the opening tag
* s single line mode
* i matches case insensitive
*/
$result = preg_replace( '/<(script|style)[^>]*?>.*?<\/\1>/si', '', $content );
return $result !== null ? $result : $content;
}
/**
* Extract image tags from content that need enhancement
*
* @param string $content Clean content.
* @return array<string> Image tag matches that need enhancement
* @since 1.4.0
*/
private function extract_image_tags( $content ): array {
$missing_alt_images = $this->extract_images_missing_alt( $content );
$missing_title_images = $this->extract_images_missing_title( $content );
if ( empty( $missing_alt_images ) && empty( $missing_title_images ) ) {
return [];
}
return array_unique( array_merge( $missing_alt_images, $missing_title_images ) );
}
/**
* Extract images missing alt attributes
*
* @param string $content Content to search.
* @return array<string> Image tags missing alt
* @since 1.4.0
*/
private function extract_images_missing_alt( $content ): array {
/**
* Finds all <img> tags that are missing proper alt attributes for accessibility compliance.
*
* Regex breakdown:
* <img : Matches literal "<img"
* (?! : Start negative lookahead (ensure pattern does NOT exist)
* [^>]* : Match any chars except ">" (stay within tag)
* alt\s*=\s* : Match "alt" + optional whitespace + "=" + optional whitespace
* ["\'] : Match opening quote (single or double)
* [^"\'\s] : Match at least one non-quote, non-whitespace character
* [^"\']* : Match remaining non-quote characters
* ["\'] : Match closing quote
* ) : End negative lookahead
* [^>]*> : Match remaining tag content until closing ">"
* i : Case-insensitive flag
*
* Examples of what this WILL match (accessibility violations):
* - <img src="photo.jpg"> (no alt attribute)
* - <img src="photo.jpg" alt=""> (empty alt)
* - <img src="photo.jpg" alt=" "> (whitespace-only alt)
* - <IMG SRC="photo.jpg" ALT=""> (case variations)
*
* Examples of what this will NOT match (valid alt attributes):
* - <img src="photo.jpg" alt="A photo"> (valid alt text)
* - <img src="photo.jpg" alt="User avatar"> (descriptive alt text)
*
* @param string $content The HTML content to search for non-compliant img tags
* @param array $matches Output array that will contain all matched img tags
* @return int Number of matches found
*
* @see https://www.php.net/manual/en/reference.pcre.pattern.syntax.php
* @since 1.0.0
*/
preg_match_all( '/<img(?![^>]*alt\s*=\s*["\'][^"\'\s][^"\']*["\'])[^>]*>/i', $content, $matches );
return $matches[0];
}
/**
* Extract images missing title attributes
*
* @param string $content Content to search.
* @return array<string> Image tags missing title
* @since 1.4.0
*/
private function extract_images_missing_title( $content ): array {
/**
* Finds all <img> tags that are missing proper title attributes for accessibility compliance.
*
* Regex breakdown:
* <img : Matches literal "<img"
* (?! : Start negative lookahead (ensure pattern does NOT exist)
* [^>]* : Match any chars except ">" (stay within tag)
* title\s*=\s* : Match "title" + optional whitespace + "=" + optional whitespace
* ["\'] : Match opening quote (single or double)
* [^"\'\s] : Match at least one non-quote, non-whitespace character
* [^"\']* : Match remaining non-quote characters
* ["\'] : Match closing quote
* ) : End negative lookahead
* [^>]*> : Match remaining tag content until closing ">"
* i : Case-insensitive flag
*
* Examples of what this WILL match (accessibility violations):
* - <img src="photo.jpg"> (no title attribute)
* - <img src="photo.jpg" title=""> (empty title)
* - <img src="photo.jpg" title=" "> (whitespace-only title)
* - <IMG SRC="photo.jpg" TITLE=""> (case variations)
*
* Examples of what this will NOT match (valid title attributes):
* - <img src="photo.jpg" title="A photo"> (valid title text)
* - <img src="photo.jpg" title="User avatar"> (descriptive title text)
*
* @param string $content The HTML content to search for non-compliant img tags
* @param array $matches Output array that will contain all matched img tags
* @return int Number of matches found
*
* @see https://www.php.net/manual/en/reference.pcre.pattern.syntax.php
* @since 1.0.0
*/
preg_match_all( '/<img(?![^>]*title\s*=\s*["\'][^"\'\s][^"\']*["\'])[^>]*>/i', $content, $matches );
return $matches[0];
}
/**
* Build processing context object
*
* @param int|null $post_id Post ID.
* @return object{title: string, slug: string, site_name: string} Context data
* @since 1.4.0
*/
private function build_processing_context( $post_id ): object {
$post = get_post( $post_id );
return (object) [
'title' => $post->post_title ?? '',
'slug' => $post->post_name ?? '',
'site_name' => get_bloginfo( 'name' ),
];
}
/**
* Enhance individual image tags
*
* @param string $content Original content.
* @param array<string> $images Image tag array.
* @param object{title: string, slug: string, site_name: string} $context Processing context.
* @return string Enhanced content
* @since 1.4.0
*/
private function enhance_image_tags( $content, $images, $context ): string {
foreach ( $images as $original_tag ) {
$enhanced_tag = $this->enhance_single_image( $original_tag, $context );
if ( $enhanced_tag !== $original_tag ) {
$content = str_replace( $original_tag, $enhanced_tag, $content );
}
}
return $content;
}
/**
* Enhance single image tag
*
* @param string $tag Original image tag.
* @param object{title: string, slug: string, site_name: string} $context Processing context.
* @return string Enhanced tag
* @since 1.4.0
*/
private function enhance_single_image( $tag, $context ): string {
$attributes = $this->parse_image_attributes( $tag );
if ( empty( $attributes ) ) {
return $tag;
}
$image_src = $this->resolve_image_source( $attributes );
if ( empty( $image_src ) ) {
return $tag;
}
$enhancements = $this->calculate_needed_enhancements( $attributes );
$enhancements = apply_filters( 'surerank_image_seo_enhancements', $enhancements, $attributes, $image_src, $context );
if ( empty( $enhancements ) ) {
return $tag;
}
return $this->apply_enhancements( $attributes, $enhancements, $image_src, $context );
}
/**
* Parse attributes from image tag
*
* @param string $tag Image tag.
* @return array<string, string> Parsed attributes
* @since 1.4.0
*/
private function parse_image_attributes( $tag ): array {
$attributes = [];
/**
* Using regex to parse image attributes
*
* Regex pattern breakdown:
* ([a-zA-Z_:][a-zA-Z0-9\-_.:]*) : Check for the attribute name.
* [a-zA-Z_:] : First char: letter, underscore, or colon
* [a-zA-Z0-9\-_.]* : Remaining chars: alphanumeric, hyphen, dot, underscore, colon
* = : Literal equals sign
* ["\'] : Opening quote (single or double)
* ([^"\']*) : Capture group 2 - Attribute value (any chars except quotes)
* ["\'] : Closing quote (single or double)
* i : Case-insensitive flag
*
* Examples of what this WILL match (accessibility violations):
* - <img src="photo.jpg"> (no alt attribute)
* - <img src="photo.jpg" alt=""> (empty alt)
* - <img src="photo.jpg" alt=" "> (whitespace-only alt)
* - <IMG SRC="photo.jpg" ALT=""> (case variations)
*
* Examples of what this will NOT match (valid alt attributes):
* - <img src="photo.jpg" alt="A photo"> (valid alt text)
* - <img src="photo.jpg" alt="User avatar"> (descriptive alt text)
*/
if ( preg_match_all( '/([a-zA-Z_:][a-zA-Z0-9\-_.:]*)=["\']([^"\']*)["\']/', $tag, $matches, PREG_SET_ORDER ) ) {
/**
* [0] => src="photo.jpg" // Full match
* [1] => src // Attribute name
* [2] => photo.jpg // Attribute value
*/
foreach ( $matches as $match ) {
$attributes[ $match[1] ] = $match[2];
}
}
return $attributes;
}
/**
* Resolve image source URL (supports lazy loading)
*
* @param array<string, string> $attributes Image attributes.
* @return string Image source
* @since 1.4.0
*/
private function resolve_image_source( $attributes ): string {
$lazy_attrs = [ 'data-src', 'data-lazy-src', 'data-layzr' ];
foreach ( $lazy_attrs as $attr ) {
if ( ! empty( $attributes[ $attr ] ) ) {
return $attributes[ $attr ];
}
}
return $attributes['src'] ?? '';
}
/**
* Calculate which enhancements are needed
*
* @param array<string, string> $attributes Current attributes.
* @return array<string, string> Needed enhancements
* @since 1.4.0
*/
private function calculate_needed_enhancements( $attributes ): array {
$needed = [];
if ( apply_filters( 'surerank_image_seo_enable_alt', true ) && empty( $attributes['alt'] ) ) {
$needed['alt'] = apply_filters( 'surerank_image_seo_alt_template', '%filename%' );
}
if ( apply_filters( 'surerank_image_seo_enable_title', true ) && empty( $attributes['title'] ) ) {
$needed['title'] = apply_filters( 'surerank_image_seo_title_template', '%title%' );
}
return $needed;
}
/**
* Apply enhancements to image attributes
*
* @param array<string, string> $attributes Original attributes.
* @param array<string, string> $enhancements Needed enhancements.
* @param string $src Image source.
* @param object{title: string, slug: string, site_name: string} $context Processing context.
* @return string Enhanced image tag
* @since 1.4.0
*/
private function apply_enhancements( $attributes, $enhancements, $src, $context ): string {
$filename = $this->extract_clean_filename( $src );
foreach ( $enhancements as $attr => $template ) {
$attributes[ $attr ] = $this->resolve_template( $template, $context, $filename );
}
return $this->build_image_tag( $attributes );
}
/**
* Extract and clean filename from URL
*
* @param string $url Image URL.
* @return string Clean filename
* @since 1.4.0
*/
private function extract_clean_filename( $url ): string {
if ( empty( $url ) ) {
return '';
}
return $this->sanitize_filename( $this->get_basename_without_extension( $url ) );
}
/**
* Get filename without extension
*
* @param string $url URL.
* @return string Basename
* @since 1.4.0
*/
private function get_basename_without_extension( $url ): string {
$filename = basename( $url );
/**
* Using regex to get the basename without the extension
* Regex pattern breakdown:
*
* \. matches a literal dot
* [^.]+ matches one or more characters that are not a dot
* $ matches the end of the string
*/
$result = preg_replace( '/\.[^.]+$/', '', $filename );
return $result !== null ? $result : $filename;
}
/**
* Sanitize filename for readability
*
* @param string $filename Raw filename.
* @return string Sanitized filename
* @since 1.4.0
*/
private function sanitize_filename( $filename ): string {
/**
* Using regex to sanitize the filename
* Regex pattern breakdown:
*
* [-_] matches a hyphen or underscore
* + matches one or more of the preceding element
* $ matches the end of the string
*/
$cleaned = preg_replace( '/[-_]+/', ' ', $filename );
$safe_cleaned = $cleaned !== null ? $cleaned : $filename;
return ucwords( trim( $safe_cleaned ) );
}
/**
* Resolve template variables
*
* @param string $template Template string.
* @param object{title: string, slug: string, site_name: string} $context Context data.
* @param string $filename Clean filename.
* @return string Resolved string
* @since 1.4.0
*/
private function resolve_template( $template, $context, $filename ): string {
if ( empty( $template ) ) {
return '';
}
$variables = $this->build_variable_map( $context, $filename );
$resolved = trim( strtr( $template, $variables ) );
return apply_filters( 'surerank_image_seo_resolved_text', $resolved, $template, $context, $filename );
}
/**
* Build variable replacement map
*
* @param object{title: string, slug: string, site_name: string} $context Context data.
* @param string $filename Filename.
* @return array<string, string> Variable mappings
* @since 1.4.0
*/
private function build_variable_map( $context, $filename ): array {
$default_vars = [
'%title%' => $context->title,
'%filename%' => $filename,
'%site_name%' => $context->site_name,
'%slug%' => $context->slug,
];
return apply_filters( 'surerank_image_seo_variable_map', $default_vars, $context, $filename );
}
/**
* Build complete image tag from attributes
*
* @param array<string, string> $attributes Attribute pairs.
* @return string Complete image tag
* @since 1.4.0
*/
private function build_image_tag( $attributes ): string {
$attr_pairs = [];
foreach ( $attributes as $name => $value ) {
$attr_pairs[] = $this->format_attribute_pair( $name, $value );
}
return sprintf( '<img %s>', implode( ' ', $attr_pairs ) );
}
/**
* Format single attribute pair
*
* @param string $name Attribute name.
* @param string $value Attribute value.
* @return string Formatted pair
* @since 1.4.0
*/
private function format_attribute_pair( $name, $value ): string {
return sprintf( '%s="%s"', esc_attr( $name ), esc_attr( $value ) );
}
}