'attachment', 'post_status' => 'inherit', 'post_mime_type' => array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ), 'posts_per_page' => -1, 'fields' => 'ids', 'meta_query' => array( 'relation' => 'OR', array( 'key' => '_wp_attachment_image_alt', 'compare' => 'NOT EXISTS', ), array( 'key' => '_wp_attachment_image_alt', 'value' => '', 'compare' => '=', ), ), ) ); foreach ( $query->posts as $attachment_id ) { $thumb = wp_get_attachment_image_src( $attachment_id, 'thumbnail' ); $full = wp_get_attachment_image_src( $attachment_id, 'full' ); $title = get_the_title( $attachment_id ); $parent_id = wp_get_post_parent_id( $attachment_id ); $location = __( 'Media Library', 'fix-experts-ai-image-alt-tag-generator' ); if ( $parent_id && 'product' === get_post_type( $parent_id ) ) { $location = __( 'WooCommerce Product', 'fix-experts-ai-image-alt-tag-generator' ); } $items[] = array( 'unique_key' => 'attachment-' . $attachment_id, 'type' => 'attachment', 'id' => $attachment_id, 'post_id' => 0, 'index' => 0, 'src' => $full ? $full[0] : '', 'thumb' => $thumb ? $thumb[0] : ( $full ? $full[0] : '' ), 'title' => $title ? $title : __( '(untitled)', 'fix-experts-ai-image-alt-tag-generator' ), 'location' => $location, 'edit_link' => get_edit_post_link( $parent_id ? $parent_id : $attachment_id, 'raw' ), 'current_alt' => '', ); } return $items; } /** * Images embedded directly inside published post/page content that lack * a non-empty alt attribute (independent of any Media Library alt meta, * since WordPress "bakes" alt text into content at insertion time). */ public static function scan_content_images() { $items = array(); $limit = (int) AIATG_Settings::get( 'content_scan_limit' ); $query = new WP_Query( array( 'post_type' => array( 'post', 'page' ), 'post_status' => 'publish', 'posts_per_page' => $limit > 0 ? $limit : 200, 'fields' => 'ids', 's' => '', ) ); foreach ( $query->posts as $post_id ) { $content = get_post_field( 'post_content', $post_id ); if ( false === stripos( $content, ' $src ) { $items[] = array( 'unique_key' => 'content-' . $post_id . '-' . $index, 'type' => 'content', 'id' => 0, 'post_id' => $post_id, 'index' => $index, 'src' => $src, 'thumb' => $src, 'title' => $title ? $title : __( '(untitled)', 'fix-experts-ai-image-alt-tag-generator' ), 'location' => get_post_type( $post_id ) === 'page' ? __( 'Page content', 'fix-experts-ai-image-alt-tag-generator' ) : __( 'Post content', 'fix-experts-ai-image-alt-tag-generator' ), 'edit_link' => get_edit_post_link( $post_id, 'raw' ), 'current_alt' => '', ); } } return $items; } /** * Parses HTML content and returns a 0-indexed map of img-tag-index => src * for every tag whose alt attribute is missing or empty. * * The index refers to the Nth tag found in document order and is * the same numbering used by save_content_alt() to write the value back. */ public static function extract_missing_alt_images( $content ) { $missing = array(); $dom = self::load_dom( $content ); if ( ! $dom ) { return $missing; } $imgs = $dom->getElementsByTagName( 'img' ); foreach ( $imgs as $index => $img ) { $alt = $img->getAttribute( 'alt' ); $src = $img->getAttribute( 'src' ); if ( '' === trim( $alt ) && ! empty( $src ) ) { $missing[ $index ] = $src; } } return $missing; } /** * Writes $alt_text into the Nth tag of $content and returns the * updated HTML. Returns WP_Error if the index can't be found, or if * expected_src is given and no longer matches (content changed since * the scan, so the index could now point at a different image). */ public static function set_content_image_alt( $content, $index, $alt_text, $expected_src = '' ) { $dom = self::load_dom( $content ); if ( ! $dom ) { return new WP_Error( 'aiatg_dom_error', __( 'Could not parse post content.', 'fix-experts-ai-image-alt-tag-generator' ) ); } $imgs = $dom->getElementsByTagName( 'img' ); if ( ! isset( $imgs[ $index ] ) ) { return new WP_Error( 'aiatg_image_not_found', __( 'That image could not be found in the post content anymore (content may have changed).', 'fix-experts-ai-image-alt-tag-generator' ) ); } if ( '' !== $expected_src && $imgs[ $index ]->getAttribute( 'src' ) !== $expected_src ) { return new WP_Error( 'aiatg_image_mismatch', __( 'The post content changed since this was scanned, so this image could not be safely updated. Please rescan.', 'fix-experts-ai-image-alt-tag-generator' ) ); } $imgs[ $index ]->setAttribute( 'alt', $alt_text ); return self::dom_to_html( $dom ); } /** * Loads HTML content into a DOMDocument wrapped in a neutral
so * DOMDocument doesn't inject / wrappers around it. */ private static function load_dom( $content ) { if ( '' === trim( $content ) ) { return false; } $internal_errors = libxml_use_internal_errors( true ); $dom = new DOMDocument( '1.0', 'UTF-8' ); $dom->loadHTML( '
' . $content . '
', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); libxml_clear_errors(); libxml_use_internal_errors( $internal_errors ); return $dom; } /** * Serializes the DOMDocument back to an HTML string, unwrapping the * neutral wrapper div added by load_dom(). */ private static function dom_to_html( DOMDocument $dom ) { $wrapper = $dom->getElementById( 'aiatg-root' ); if ( ! $wrapper ) { return new WP_Error( 'aiatg_dom_error', __( 'Could not reassemble post content.', 'fix-experts-ai-image-alt-tag-generator' ) ); } $html = ''; foreach ( $wrapper->childNodes as $child ) { $html .= $dom->saveHTML( $child ); } return $html; } }