Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 119 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
SiteGenImageService | |
0.00% |
0 / 119 |
|
0.00% |
0 / 5 |
1332 | |
0.00% |
0 / 1 |
process_homepage_images_immediate_async | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
extract_all_image_urls | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
upload_images_to_wp_media_library | |
0.00% |
0 / 65 |
|
0.00% |
0 / 1 |
342 | |||
update_post_content_with_new_image_urls | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
90 | |||
connect_to_filesystem | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | namespace NewfoldLabs\WP\Module\Onboarding\Services; |
4 | |
5 | use NewfoldLabs\WP\Module\Onboarding\TaskManagers\ImageSideloadTaskManager; |
6 | use 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 | */ |
13 | class 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 | $image_urls = self::extract_all_image_urls( $content ); |
25 | |
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 | * Extract all image URLs from content specifically targeting Unsplash and patterns.hiive.cloud domains. |
42 | * |
43 | * @param string $content The content to extract image URLs from. |
44 | * @return array Array of unique image URLs. |
45 | */ |
46 | private static function extract_all_image_urls( $content ) { |
47 | $image_urls = array(); |
48 | |
49 | // Extract Unsplash images |
50 | preg_match_all( '/https?:\/\/([^\/]+\.)?unsplash\.com\/[^\s"\'<>]+/i', $content, $matches ); |
51 | if ( isset( $matches[0] ) ) { |
52 | $image_urls = array_merge( $image_urls, $matches[0] ); |
53 | } |
54 | |
55 | // Extract patterns.hiive.cloud images |
56 | preg_match_all( '/https?:\/\/patterns\.hiive\.cloud\/[^\s"\'<>]+/i', $content, $matches ); |
57 | if ( isset( $matches[0] ) ) { |
58 | $image_urls = array_merge( $image_urls, $matches[0] ); |
59 | } |
60 | |
61 | // Decode HTML entities in URLs to ensure proper replacement |
62 | $image_urls = array_map( 'html_entity_decode', $image_urls ); |
63 | |
64 | return array_values( array_unique( $image_urls ) ); |
65 | } |
66 | |
67 | /** |
68 | * Uploads images to the WordPress media library as attachments. |
69 | * |
70 | * This function takes an array of image URLs, downloads them, and |
71 | * uploads them to the WordPress media library, returning the URLs |
72 | * of the newly uploaded images. |
73 | * |
74 | * @param array $image_urls An array of image URLs to upload. |
75 | * @param int $post_id The post ID to attach the images to. |
76 | * @return array|false An array of WordPress attachment URLs on success, false on failure. |
77 | * @throws Exception If there is an error during the upload process. |
78 | */ |
79 | public static function upload_images_to_wp_media_library( $image_urls, $post_id ) { |
80 | require_once ABSPATH . 'wp-admin/includes/media.php'; |
81 | require_once ABSPATH . 'wp-admin/includes/image.php'; |
82 | |
83 | global $wp_filesystem; |
84 | self::connect_to_filesystem(); |
85 | |
86 | $uploaded_image_urls = array(); |
87 | $total_images = count( $image_urls ); |
88 | $successful_uploads = 0; |
89 | |
90 | try { |
91 | foreach ( $image_urls as $image_url ) { |
92 | // Check if the URL is valid. |
93 | if ( ! filter_var( $image_url, FILTER_VALIDATE_URL ) ) { |
94 | continue; |
95 | } |
96 | |
97 | // Fetch the image via remote get with timeout and a retry attempt. |
98 | $attempt = 0; |
99 | $max_attempts = 2; |
100 | while ( $attempt < $max_attempts ) { |
101 | $response = wp_remote_get( $image_url, array( 'timeout' => 15 ) ); |
102 | if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) { |
103 | break; |
104 | } |
105 | ++$attempt; |
106 | } |
107 | if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { |
108 | continue; |
109 | } |
110 | // Reading the headers from the image url to determine. |
111 | $headers = wp_remote_retrieve_headers( $response ); |
112 | $content_type = $headers['content-type'] ?? ''; |
113 | $image_data = wp_remote_retrieve_body( $response ); |
114 | if ( empty( $content_type ) || empty( $image_data ) ) { |
115 | continue; |
116 | } |
117 | // Determine the file extension based on MIME type. |
118 | $file_extension = ''; |
119 | switch ( $content_type ) { |
120 | case 'image/jpeg': |
121 | $file_extension = '.jpg'; |
122 | break; |
123 | case 'image/png': |
124 | $file_extension = '.png'; |
125 | break; |
126 | case 'image/gif': |
127 | $file_extension = '.gif'; |
128 | break; |
129 | case 'image/webp': |
130 | $file_extension = '.webp'; |
131 | break; |
132 | } |
133 | |
134 | if ( '' === $file_extension ) { |
135 | continue; |
136 | } |
137 | // create upload directory. |
138 | $upload_dir = wp_upload_dir(); |
139 | // xtract a filename from the URL. |
140 | $parsed_url = wp_parse_url( $image_url ); |
141 | $path_parts = pathinfo( $parsed_url['path'] ); |
142 | // filename to be added in directory. |
143 | $original_filename = $path_parts['filename'] . $file_extension; |
144 | |
145 | // to ensure the filename is unique within the upload directory. |
146 | $filename = wp_unique_filename( $upload_dir['path'], $original_filename ); |
147 | $filepath = $upload_dir['path'] . '/' . $filename; |
148 | |
149 | $wp_filesystem->put_contents( $filepath, $image_data ); |
150 | |
151 | // Create an attachment post for the image, metadata needed for WordPress media library. |
152 | // guid -for url, post_title for cleaned up name, post content is empty as this is an attachment. |
153 | // post_status inherit is for visibility. |
154 | $attachment = array( |
155 | 'guid' => $upload_dir['url'] . '/' . $filename, |
156 | 'post_mime_type' => $content_type, |
157 | 'post_title' => preg_replace( '/\.[^.]+$/', '', basename( $filename ) ), |
158 | 'post_content' => '', |
159 | 'post_status' => 'inherit', |
160 | 'post_parent' => $post_id, // Attach to the specified post |
161 | ); |
162 | $attach_id = wp_insert_attachment( $attachment, $filepath ); |
163 | |
164 | // Generate and assign metadata for the attachment. |
165 | $attach_data = wp_generate_attachment_metadata( $attach_id, $filepath ); |
166 | wp_update_attachment_metadata( $attach_id, $attach_data ); |
167 | |
168 | // Add the WordPress attachment URL to the list. |
169 | if ( $attach_id ) { |
170 | $attachment_url = wp_get_attachment_url( $attach_id ); |
171 | if ( ! $attachment_url ) { |
172 | $attachment_url = null; |
173 | } |
174 | $uploaded_image_urls[ $image_url ] = $attachment_url; |
175 | $successful_uploads++; |
176 | } |
177 | } |
178 | } catch ( \Exception $e ) { |
179 | // Log error silently |
180 | } |
181 | return $uploaded_image_urls; |
182 | } |
183 | |
184 | /** |
185 | * Update post content by replacing original image URLs with WordPress media library URLs. |
186 | * |
187 | * @param int $post_id The post ID to update. |
188 | * @param array $url_mapping Array mapping original URLs to new WordPress URLs. |
189 | * @return bool True on success, false on failure. |
190 | */ |
191 | public static function update_post_content_with_new_image_urls( $post_id, $url_mapping ) { |
192 | // Get the current post content |
193 | $post = get_post( $post_id ); |
194 | if ( ! $post ) { |
195 | return false; |
196 | } |
197 | |
198 | $content = $post->post_content; |
199 | $updated = false; |
200 | $replaced_count = 0; |
201 | |
202 | // Replace each original URL with the new WordPress URL |
203 | foreach ( $url_mapping as $original_url => $new_url ) { |
204 | if ( ! empty( $new_url ) ) { |
205 | // Use str_replace for exact URL replacement |
206 | $new_content = str_replace( $original_url, $new_url, $content ); |
207 | |
208 | // If no replacement happened, try with HTML entity encoded version |
209 | if ( $new_content === $content ) { |
210 | $encoded_url = htmlspecialchars( $original_url, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); |
211 | $new_content = str_replace( $encoded_url, $new_url, $content ); |
212 | } |
213 | |
214 | // If still no replacement, try with double-encoded version (common in WordPress) |
215 | if ( $new_content === $content ) { |
216 | $double_encoded_url = htmlspecialchars( htmlspecialchars( $original_url, ENT_QUOTES | ENT_HTML5, 'UTF-8' ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); |
217 | $new_content = str_replace( $double_encoded_url, $new_url, $content ); |
218 | } |
219 | |
220 | if ( $new_content !== $content ) { |
221 | $content = $new_content; |
222 | $updated = true; |
223 | $replaced_count++; |
224 | } |
225 | } |
226 | } |
227 | |
228 | // Update the post if content changed |
229 | if ( $updated ) { |
230 | $update_result = wp_update_post( |
231 | array( |
232 | 'ID' => $post_id, |
233 | 'post_content' => $content, |
234 | ) |
235 | ); |
236 | |
237 | if ( is_wp_error( $update_result ) ) { |
238 | return false; |
239 | } |
240 | |
241 | return true; |
242 | } |
243 | |
244 | return true; // No changes needed |
245 | } |
246 | |
247 | /** |
248 | * Connect to the WordPress filesystem. |
249 | * |
250 | * @return boolean |
251 | */ |
252 | public static function connect_to_filesystem() { |
253 | require_once ABSPATH . 'wp-admin/includes/file.php'; |
254 | |
255 | // We want to ensure that the user has direct access to the filesystem. |
256 | $access_type = \get_filesystem_method(); |
257 | if ( 'direct' !== $access_type ) { |
258 | return false; |
259 | } |
260 | |
261 | $creds = \request_filesystem_credentials( site_url() . '/wp-admin', '', false, false, array() ); |
262 | |
263 | if ( ! \WP_Filesystem( $creds ) ) { |
264 | return false; |
265 | } |
266 | |
267 | return true; |
268 | } |
269 | } |