classification_service = $classification_service; } /** * Wraps existing image markup without rebuilding or changing its attributes. * * @param int $attachment_id Attachment ID. * @param string $image_markup Existing img, picture, or linked-image markup. * @param string|null $classification Optional preloaded user classification. * @param array|null $decision Optional preloaded rule decision. * @param array|null $settings Optional preloaded plugin settings. * @return string */ public function render( $attachment_id, $image_markup, $classification = null, $decision = null, $settings = null ) { $attachment_id = absint( $attachment_id ); $image_markup = (string) $image_markup; if ( ! $attachment_id || '' === $image_markup || false !== strpos( $image_markup, 'aiparently-image' ) || false !== strpos( $image_markup, 'data-aiparently-processed' ) ) { return $image_markup; } if ( 'attachment' !== get_post_type( $attachment_id ) ) { return $image_markup; } $settings = is_array( $settings ) ? $settings : AIparently_Disclosure_Service::get_settings(); if ( empty( $settings['enabled'] ) || empty( $settings['images_enabled'] ) ) { return $image_markup; } $classification = null === $classification ? $this->classification_service->get_status( $attachment_id ) : AIparently_Classification_Service::sanitize_status( $classification ); $decision = is_array( $decision ) ? $decision : $this->classification_service->get_decision( $attachment_id ); if ( ! in_array( $classification, array( 'assisted', 'generated' ), true ) || empty( $decision['required'] ) || empty( $decision['label'] ) ) { return $image_markup; } $style = isset( $settings['style'] ) ? sanitize_html_class( $settings['style'] ) : 'badge'; $position = isset( $settings['image_label_position'] ) && in_array( $settings['image_label_position'], array( 'left', 'center', 'right' ), true ) ? $settings['image_label_position'] : 'right'; $image_markup = $this->mark_first_image_processed( $image_markup ); return sprintf( '%3$s%8$s', $attachment_id, esc_attr( $classification ), $image_markup, esc_attr( $style ), esc_attr( $position ), esc_attr( $decision['code'] ), esc_attr( $decision['ruleset_version'] ), esc_html( $decision['label'] ) ); } /** * Marks the original img so later WordPress content filters cannot wrap it * a second time. All existing responsive, lazy-loading, style, and builder * attributes remain untouched. */ private function mark_first_image_processed( $image_markup ) { if ( false !== strpos( $image_markup, 'data-aiparently-processed' ) ) { return $image_markup; } if ( class_exists( 'WP_HTML_Tag_Processor' ) ) { $processor = new WP_HTML_Tag_Processor( $image_markup ); if ( $processor->next_tag( 'IMG' ) ) { $processor->set_attribute( 'data-aiparently-processed', '1' ); return $processor->get_updated_html(); } } return preg_replace( '/]*>\s*]*>.*?<\/picture>\s*<\/a>/is', '/]*>\s*]*>\s*<\/a>/is', '/]*>.*?<\/picture>/is', '/]*>/is', ); foreach ( $patterns as $pattern ) { if ( preg_match( $pattern, $component_markup ) ) { $renderer = $this; return preg_replace_callback( $pattern, static function ( $matches ) use ( $renderer, $attachment_id ) { return $renderer->render( $attachment_id, $matches[0] ); }, $component_markup, 1 ); } } return $component_markup; } /** * Finds media-library images in one already-scoped block/widget and renders * every classified image. This never receives or scans a complete page. */ public function render_detected_images( $component_markup, $known_attachment_id = 0 ) { $component_markup = (string) $component_markup; if ( '' === $component_markup || false !== strpos( $component_markup, 'aiparently-image' ) ) { return $component_markup; } $renderer = $this; $known_attachment_id = absint( $known_attachment_id ); $match_index = 0; return preg_replace_callback( '/]*>.*?<\/picture>|]*>/is', static function ( $matches ) use ( $renderer, $known_attachment_id, &$match_index ) { $attachment_id = 0 === $match_index && $known_attachment_id ? $known_attachment_id : $renderer->resolve_attachment_id( $matches[0] ); ++$match_index; return $attachment_id ? $renderer->render( $attachment_id, $matches[0] ) : $matches[0]; }, $component_markup ); } /** * Resolves the attachment behind an img/picture without relying on a theme * class. Explicit IDs win; media URLs are the compatible fallback. */ public function resolve_attachment_id( $image_markup ) { $image_markup = (string) $image_markup; $attachment_id = 0; $class_name = ''; $src = ''; if ( class_exists( 'WP_HTML_Tag_Processor' ) ) { $processor = new WP_HTML_Tag_Processor( $image_markup ); if ( $processor->next_tag( 'IMG' ) ) { $attachment_id = absint( $processor->get_attribute( 'data-attachment-id' ) ); $class_name = (string) $processor->get_attribute( 'class' ); $src = (string) $processor->get_attribute( 'src' ); } } else { if ( preg_match( '/\bdata-attachment-id=["\'](\d+)["\']/i', $image_markup, $match ) ) { $attachment_id = absint( $match[1] ); } if ( preg_match( '/\bclass=["\']([^"\']*)["\']/i', $image_markup, $match ) ) { $class_name = $match[1]; } if ( preg_match( '/\bsrc=["\']([^"\']+)["\']/i', $image_markup, $match ) ) { $src = html_entity_decode( $match[1], ENT_QUOTES, 'UTF-8' ); } } if ( ! $attachment_id && preg_match( '/(?:^|\s)wp-image-(\d+)(?:\s|$)/', $class_name, $match ) ) { $attachment_id = absint( $match[1] ); } $src = strtok( $src, '?#' ); if ( ! $attachment_id && $src ) { if ( isset( $this->resolved_urls[ $src ] ) ) { $attachment_id = $this->resolved_urls[ $src ]; } else { $attachment_id = absint( attachment_url_to_postid( $src ) ); if ( ! $attachment_id ) { $original_src = preg_replace( '/-\d+x\d+(?=\.(?:avif|gif|jpe?g|png|webp)$)/i', '', $src ); $attachment_id = $original_src !== $src ? absint( attachment_url_to_postid( $original_src ) ) : 0; } $this->resolved_urls[ $src ] = $attachment_id; } } return absint( apply_filters( 'aiparently_image_attachment_id', $attachment_id, $src, $image_markup ) ); } }