Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 105
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
SiteGenImageService
0.00% covered (danger)
0.00%
0 / 105
0.00% covered (danger)
0.00%
0 / 4
1056
0.00% covered (danger)
0.00%
0 / 1
 process_homepage_images_immediate_async
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 upload_images_to_wp_media_library
0.00% covered (danger)
0.00%
0 / 65
0.00% covered (danger)
0.00%
0 / 1
342
 update_post_content_with_new_image_urls
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
56
 connect_to_filesystem
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace NewfoldLabs\WP\Module\Onboarding\Services;
4
5use NewfoldLabs\WP\Module\Onboarding\TaskManagers\ImageSideloadTaskManager;
6use NewfoldLabs\WP\Module\Onboarding\Tasks\ImageSideloadTask;
7
8/**
9 * SiteGenImageService for the onboarding module.
10 *
11 * Handles image processing and media library operations for the onboarding flow.
12 */
13class SiteGenImageService {
14
15    /**
16     * Process homepage images immediately in background (non-blocking).
17     * This method dispatches an async request that doesn't block the main request.
18     *
19     * @param int    $post_id The post ID to process images for.
20     * @param string $content The content containing images.
21     */
22    public static function process_homepage_images_immediate_async( $post_id, $content ) {
23        // Extract image URLs from content
24        preg_match_all( '/<img[^>]+src=["\']([^"\']+)["\']/i', $content, $matches );
25        $image_urls = isset( $matches[1] ) ? $matches[1] : array();
26        if ( empty( $image_urls ) ) {
27            return;
28        }
29
30        // Create and add task to queue
31        $task = new ImageSideloadTask( $post_id, $image_urls );
32        ImageSideloadTaskManager::add_to_queue( $task );
33
34        // Schedule a single event to process the queue (if not already scheduled)
35        if ( ! wp_next_scheduled( 'nfd_process_image_sideload_queue' ) ) {
36            wp_schedule_single_event( time(), 'nfd_process_image_sideload_queue' );
37        }
38    }
39
40    /**
41     * Uploads images to the WordPress media library as attachments.
42     *
43     * This function takes an array of image URLs, downloads them, and
44     * uploads them to the WordPress media library, returning the URLs
45     * of the newly uploaded images.
46     *
47     * @param array $image_urls An array of image URLs to upload.
48     * @param int   $post_id The post ID to attach the images to.
49     * @return array|false An array of WordPress attachment URLs on success, false on failure.
50     * @throws Exception If there is an error during the upload process.
51     */
52    public static function upload_images_to_wp_media_library( $image_urls, $post_id ) {
53        require_once ABSPATH . 'wp-admin/includes/media.php';
54        require_once ABSPATH . 'wp-admin/includes/image.php';
55
56        global $wp_filesystem;
57        self::connect_to_filesystem();
58
59        $uploaded_image_urls = array();
60        $total_images = count( $image_urls );
61        $successful_uploads  = 0;
62
63        try {
64            foreach ( $image_urls as $image_url ) {
65                // Check if the URL is valid.
66                if ( ! filter_var( $image_url, FILTER_VALIDATE_URL ) ) {
67                    continue;
68                }
69
70                // Fetch the image via remote get with timeout and a retry attempt.
71                $attempt      = 0;
72                $max_attempts = 2;
73                while ( $attempt < $max_attempts ) {
74                    $response = wp_remote_get( $image_url, array( 'timeout' => 15 ) );
75                    if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
76                        break;
77                    }
78                    ++$attempt;
79                }
80                if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
81                    continue;
82                }
83                // Reading the headers from the image url to determine.
84                $headers      = wp_remote_retrieve_headers( $response );
85                $content_type = $headers['content-type'] ?? '';
86                $image_data   = wp_remote_retrieve_body( $response );
87                if ( empty( $content_type ) || empty( $image_data ) ) {
88                    continue;
89                }
90                // Determine the file extension based on MIME type.
91                $file_extension = '';
92                switch ( $content_type ) {
93                    case 'image/jpeg':
94                        $file_extension = '.jpg';
95                        break;
96                    case 'image/png':
97                        $file_extension = '.png';
98                        break;
99                    case 'image/gif':
100                        $file_extension = '.gif';
101                        break;
102                    case 'image/webp':
103                        $file_extension = '.webp';
104                        break;
105                }
106
107                if ( '' === $file_extension ) {
108                    continue;
109                }
110                // create upload directory.
111                $upload_dir = wp_upload_dir();
112                // xtract a filename from the URL.
113                $parsed_url = wp_parse_url( $image_url );
114                $path_parts = pathinfo( $parsed_url['path'] );
115                // filename to be added in directory.
116                $original_filename = $path_parts['filename'] . $file_extension;
117
118                // to ensure the filename is unique within the upload directory.
119                $filename = wp_unique_filename( $upload_dir['path'], $original_filename );
120                $filepath = $upload_dir['path'] . '/' . $filename;
121
122                $wp_filesystem->put_contents( $filepath, $image_data );
123
124                // Create an attachment post for the image, metadata needed for WordPress media library.
125                // guid -for url, post_title for cleaned up name, post content is empty as this is an attachment.
126                // post_status inherit is for visibility.
127                $attachment = array(
128                    'guid'           => $upload_dir['url'] . '/' . $filename,
129                    'post_mime_type' => $content_type,
130                    'post_title'     => preg_replace( '/\.[^.]+$/', '', basename( $filename ) ),
131                    'post_content'   => '',
132                    'post_status'    => 'inherit',
133                    'post_parent'    => $post_id, // Attach to the specified post
134                );
135                $attach_id  = wp_insert_attachment( $attachment, $filepath );
136
137                // Generate and assign metadata for the attachment.
138                $attach_data = wp_generate_attachment_metadata( $attach_id, $filepath );
139                wp_update_attachment_metadata( $attach_id, $attach_data );
140
141                // Add the WordPress attachment URL to the list.
142                if ( $attach_id ) {
143                    $attachment_url = wp_get_attachment_url( $attach_id );
144                    if ( ! $attachment_url ) {
145                        $attachment_url = null;
146                    }
147                    $uploaded_image_urls[ $image_url ] = $attachment_url;
148                    $successful_uploads++;
149                }
150            }
151        } catch ( \Exception $e ) {
152            // Log error silently
153        }
154        return $uploaded_image_urls;
155    }
156
157    /**
158     * Update post content by replacing original image URLs with WordPress media library URLs.
159     *
160     * @param int   $post_id The post ID to update.
161     * @param array $url_mapping Array mapping original URLs to new WordPress URLs.
162     * @return bool True on success, false on failure.
163     */
164    public static function update_post_content_with_new_image_urls( $post_id, $url_mapping ) {
165        // Get the current post content
166        $post = get_post( $post_id );
167        if ( ! $post ) {
168            return false;
169        }
170
171        $content        = $post->post_content;
172        $updated        = false;
173        $replaced_count = 0;
174
175        // Replace each original URL with the new WordPress URL
176        foreach ( $url_mapping as $original_url => $new_url ) {
177            if ( ! empty( $new_url ) ) {
178                // Use str_replace for exact URL replacement
179                $new_content = str_replace( $original_url, $new_url, $content );
180                if ( $new_content !== $content ) {
181                    $content = $new_content;
182                    $updated = true;
183                    $replaced_count++;
184                }
185            }
186        }
187
188        // Update the post if content changed
189        if ( $updated ) {
190            $update_result = wp_update_post(
191                array(
192                    'ID'           => $post_id,
193                    'post_content' => $content,
194                )
195            );
196
197            if ( is_wp_error( $update_result ) ) {
198                return false;
199            }
200
201            return true;
202        }
203
204        return true; // No changes needed
205    }
206
207    /**
208     * Connect to the WordPress filesystem.
209     *
210     * @return boolean
211     */
212    public static function connect_to_filesystem() {
213        require_once ABSPATH . 'wp-admin/includes/file.php';
214
215        // We want to ensure that the user has direct access to the filesystem.
216        $access_type = \get_filesystem_method();
217        if ( 'direct' !== $access_type ) {
218            return false;
219        }
220
221        $creds = \request_filesystem_credentials( site_url() . '/wp-admin', '', false, false, array() );
222
223        if ( ! \WP_Filesystem( $creds ) ) {
224            return false;
225        }
226
227        return true;
228    }
229}