min_width = $min_width; $this->min_height = $min_height; $this->log_manager = new AITM_Log_Manager(); } /** * Find first suitable image in post content * * @param int $post_id Post ID. * @return array|null Image data or null if not found. */ public function find_first_image( $post_id ) { $post = get_post( $post_id ); if ( ! $post || empty( $post->post_content ) ) { return null; } // Process shortcodes before searching for images $content = do_shortcode( $post->post_content ); // Find all images in content $images = $this->find_all_images( $content ); if ( empty( $images ) ) { return null; } // Log: количество найденных изображений $this->log_debug( sprintf( 'Post ID %d: Found %d images in content', $post_id, count( $images ) ) ); // Iterate through images to find first suitable one $index = 0; foreach ( $images as $image ) { $index++; $image_url = $image['url'] ?? 'unknown'; $short_url = basename( $image_url ); $is_bg_img = empty( $image['tag'] ); $image_type = $is_bg_img ? 'background image' : 'image'; // Try to get attachment ID $attachment_id = $this->get_thumbnail_id( $image ); // If no attachment found, check if it's local or external if ( ! $attachment_id ) { $is_local_url = $this->is_local_url( $image_url ); if ( $is_local_url ) { // Local URL but attachment not found - skip with error $this->log_debug( sprintf( 'Post ID %d: %s #%d (%s) - SKIPPED: Attachment not found in database (ID from tag does not match attachment or not found)', $post_id, ucfirst( $image_type ), $index, $short_url ) ); continue; } // External image - return it so it can be downloaded if download_remote_images is enabled $this->log_debug( sprintf( 'Post ID %d: %s #%d (%s) - External image, will try to download if enabled', $post_id, ucfirst( $image_type ), $index, $short_url ) ); return $image; } // Check if attachment exists and is an image $is_image = $this->verify_attachment_is_image( $attachment_id ); if ( ! $is_image['valid'] ) { $this->log_debug( sprintf( 'Post ID %d: %s #%d (%s, ID: %d) - SKIPPED: %s', $post_id, ucfirst( $image_type ), $index, $short_url, $attachment_id, $is_image['reason'] ) ); continue; } // Check size requirements if ( ! $this->check_image_size( $attachment_id ) ) { $metadata = wp_get_attachment_metadata( $attachment_id ); $width = $metadata['width'] ?? 0; $height = $metadata['height'] ?? 0; $this->log_debug( sprintf( 'Post ID %d: %s #%d (%s, ID: %d) - SKIPPED: does not meet size requirements (%dx%d, min: %dx%d)', $post_id, ucfirst( $image_type ), $index, $short_url, $attachment_id, $width, $height, $this->min_width, $this->min_height ) ); continue; } // Found suitable image $this->log_debug( sprintf( 'Post ID %d: %s #%d (%s, ID: %d) - SELECTED as featured image', $post_id, ucfirst( $image_type ), $index, $short_url, $attachment_id ) ); return $image; } // No suitable image found $this->log_debug( sprintf( 'Post ID %d: No suitable image found after checking %d images', $post_id, count( $images ) ) ); return null; } /** * Find all images in content * * @param string $content Post content. * @return array Array of image data. */ private function find_all_images( $content ) { $images = array(); $matches = array(); // Get all img tags using regex preg_match_all( '/<\s*img\s+.*?src\s*=\s*[\"\']?([^\"\'> ]*).*?>/i', $content, $matches ); if ( ! empty( $matches[0] ) ) { foreach ( $matches[0] as $key => $img_tag ) { $url = $matches[1][ $key ] ?? ''; if ( empty( $url ) ) { continue; } // Extract title attribute if exists $title = ''; $title_matches = array(); preg_match_all( '/<\s*img\s+[^\>]*title\s*=\s*[\"\']?([^\"\'> ]*)/i', $img_tag, $title_matches ); if ( ! empty( $title_matches[1][0] ) ) { $title = $title_matches[1][0]; } $images[] = array( 'tag' => $img_tag, 'url' => $url, 'title' => $title, ); } } // Find background images in style attributes // Example: style="background-image:url(http://example.com/image.jpg)" $bg_matches = array(); preg_match_all( '/style\s*=\s*["\'][^"\']*background-image\s*:\s*url\s*\(\s*["\']?([^"\'\)]+)["\']?\s*\)[^"\']*["\']/i', $content, $bg_matches ); if ( ! empty( $bg_matches[1] ) ) { foreach ( $bg_matches[1] as $url ) { if ( empty( $url ) ) { continue; } $images[] = array( 'tag' => '', // No tag for background images 'url' => trim( $url ), 'title' => '', ); } } return $images; } /** * Get thumbnail ID from image data * * @param array $image Image data with 'tag' and 'url'. * @return int|false Attachment ID or false. */ public function get_thumbnail_id( $image ) { global $wpdb; $thumb_id = 0; // First, check if there's a wp-image-{ID} class in the tag // WordPress automatically adds this class for media library images if ( isset( $image['tag'] ) && ! empty( $image['tag'] ) ) { $class_matches = array(); preg_match( '/wp-image-([\d]+)/i', $image['tag'], $class_matches ); if ( ! empty( $class_matches[1] ) ) { $thumb_id = (int) $class_matches[1]; // Check if this attachment exists and is actually an attachment $post = get_post( $thumb_id ); if ( $post && $post->post_type === 'attachment' ) { return $thumb_id; } $thumb_id = 0; } } // If not found by class, search by URL in database if ( ! $thumb_id && isset( $image['url'] ) && ! empty( $image['url'] ) ) { $image_url = $image['url']; // Remove size suffixes from URL (-150x150, -300x200, etc.) // to find the original image $image_url = preg_replace( '/-[0-9]{1,4}x[0-9]{1,4}\./i', '.', $image_url ); // Try to get from cache $cache_key = 'aitm_img_' . md5( $image_url ); $thumb_id = wp_cache_get( $cache_key, 'aitm' ); if ( false === $thumb_id ) { // Extract path from uploads directory // Example: http://localhost:8000/wp-content/uploads/2021/03/image.jpg -> uploads/2021/03/image.jpg $upload_path = ''; if ( preg_match( '#/uploads/(.+)$#', $image_url, $matches ) ) { $upload_path = 'uploads/' . $matches[1]; } // Search in database by guid path if ( ! empty( $upload_path ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $thumb_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE guid LIKE %s AND post_type = 'attachment' ORDER BY ID DESC LIMIT 1", '%' . $wpdb->esc_like( $upload_path ) ) ); } // Cache for 1 hour if ( $thumb_id ) { wp_cache_set( $cache_key, $thumb_id, 'aitm', 3600 ); } } if ( $thumb_id ) { return (int) $thumb_id; } } return false; } /** * Check if image meets size requirements * * @param int $attachment_id Attachment ID. * @return bool True if meets requirements. */ public function check_image_size( $attachment_id ) { // If no requirements set, image is suitable if ( $this->min_width <= 0 && $this->min_height <= 0 ) { return true; } $metadata = wp_get_attachment_metadata( $attachment_id ); if ( empty( $metadata ) || ! isset( $metadata['width'], $metadata['height'] ) ) { return false; } $width = $metadata['width']; $height = $metadata['height']; if ( $this->min_width > 0 && $width < $this->min_width ) { return false; } if ( $this->min_height > 0 && $height < $this->min_height ) { return false; } return true; } /** * Verify that attachment is a valid image * * Uses multiple checks to ensure attachment is an image: * 1. WordPress built-in wp_attachment_is_image() * 2. File extension check via wp_check_filetype() * 3. MIME type check from post meta * * @param int $attachment_id Attachment ID. * @return array Array with 'valid' (bool) and 'reason' (string) keys. */ private function verify_attachment_is_image( $attachment_id ) { // First, try WordPress built-in function if ( wp_attachment_is_image( $attachment_id ) ) { return array( 'valid' => true, 'reason' => '', ); } // Get attachment post $attachment = get_post( $attachment_id ); if ( ! $attachment ) { return array( 'valid' => false, 'reason' => 'attachment post not found', ); } // Check post type if ( $attachment->post_type !== 'attachment' ) { return array( 'valid' => false, 'reason' => 'not an attachment', ); } // Get file path $file = get_attached_file( $attachment_id ); if ( ! $file ) { return array( 'valid' => false, 'reason' => 'file path not found', ); } // Check file exists if ( ! file_exists( $file ) ) { return array( 'valid' => false, 'reason' => 'file does not exist on disk', ); } // Check file type by extension $filetype = wp_check_filetype( $file ); if ( empty( $filetype['type'] ) ) { return array( 'valid' => false, 'reason' => 'unable to determine file type', ); } // Check if MIME type starts with 'image/' if ( ! str_starts_with( $filetype['type'], 'image/' ) ) { return array( 'valid' => false, 'reason' => sprintf( 'not an image (type: %s)', $filetype['type'] ), ); } // All checks passed return array( 'valid' => true, 'reason' => '', ); } /** * Check if URL belongs to current site * * @param string $url URL to check. * @return bool True if URL is local. */ private function is_local_url( $url ) { if ( empty( $url ) ) { return false; } // Parse URL $parsed_url = wp_parse_url( $url ); if ( ! $parsed_url || ! isset( $parsed_url['host'] ) ) { // Relative URL - consider as local if ( strpos( $url, '/' ) === 0 || strpos( $url, 'wp-content' ) !== false ) { return true; } return false; } $url_host = strtolower( $parsed_url['host'] ); // Get site URLs $home_url = wp_parse_url( home_url() ); $site_url = wp_parse_url( site_url() ); $home_host = isset( $home_url['host'] ) ? strtolower( $home_url['host'] ) : ''; $site_host = isset( $site_url['host'] ) ? strtolower( $site_url['host'] ) : ''; // Check if host matches if ( $url_host === $home_host || $url_host === $site_host ) { return true; } // Check for localhost variations $localhost_patterns = array( 'localhost', '127.0.0.1', '::1' ); $is_localhost = false; foreach ( $localhost_patterns as $pattern ) { if ( strpos( $url_host, $pattern ) !== false ) { $is_localhost = true; break; } } // If both are localhost (even with different ports), consider as local if ( $is_localhost ) { foreach ( $localhost_patterns as $pattern ) { if ( strpos( $home_host, $pattern ) !== false || strpos( $site_host, $pattern ) !== false ) { return true; } } } return false; } /** * Log debug message * * @param string $message Debug message. * @return void */ private function log_debug( $message ) { $this->log_manager->write( 'debug', $message ); } }