Skip to content

Commit 0c8cd20

Browse files
authored
Merge pull request #816 from newfold-labs/update/homepage-image-processing
side load images for the homepage
2 parents cdb2c1f + 11062d0 commit 0c8cd20

File tree

5 files changed

+585
-12
lines changed

5 files changed

+585
-12
lines changed

bootstrap.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use NewfoldLabs\WP\Module\Onboarding\Compatibility\Scan;
77
use NewfoldLabs\WP\Module\Onboarding\Compatibility\Safe_Mode;
88
use NewfoldLabs\WP\Module\Onboarding\Compatibility\Status;
9+
use NewfoldLabs\WP\Module\Onboarding\TaskManagers\ImageSideloadTaskManager;
10+
use NewfoldLabs\WP\Module\Onboarding\Tasks\ImageSideloadTask;
911

1012
use function NewfoldLabs\WP\ModuleLoader\register;
1113

@@ -80,4 +82,7 @@ function () {
8082
delete_transient( 'nfd_site_capabilities' );
8183
}
8284
);
85+
86+
// Add action to process image sideload queue
87+
add_action( 'nfd_process_image_sideload_queue', array( ImageSideloadTaskManager::class, 'process_queue' ) );
8388
}
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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+
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+
}

includes/Services/SiteGenService.php

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,39 @@
44

55
use NewfoldLabs\WP\Module\Onboarding\Data\Services\SitePagesService;
66
use NewfoldLabs\WP\Module\Onboarding\Data\Services\SiteGenService as LegacySiteGenService;
7-
7+
use NewfoldLabs\WP\Module\Onboarding\Services\SiteGenImageService;
8+
use NewfoldLabs\WP\Module\Onboarding\Services\ReduxStateService;
9+
10+
/**
11+
* Class SiteGenService
12+
*
13+
* Handles the onboarding SiteGen flow for generating, publishing, and managing AI-generated homepages and related content.
14+
*
15+
* This service is designed to work with the onboarding module's Redux state and integrates with other onboarding services.
16+
*
17+
* @package NewfoldLabs\WP\Module\Onboarding\Services
18+
*/
819
class SiteGenService {
920

1021
/**
1122
* The Redux input object.
1223
*
1324
* @var array|null
1425
*/
15-
private ?array $input_data = null;
16-
26+
private $input_data = null;
27+
1728
/**
1829
* The Redux sitegen object.
1930
*
2031
* @var array|null
2132
*/
22-
private ?array $sitegen_data = null;
33+
private $sitegen_data = null;
2334

35+
/**
36+
* SiteGenService constructor.
37+
*
38+
* Initializes the service by loading the Redux input and sitegen data from the ReduxStateService.
39+
*/
2440
public function __construct() {
2541
$this->input_data = ReduxStateService::get( 'input' );
2642
$this->sitegen_data = ReduxStateService::get( 'sitegen' );
@@ -32,7 +48,7 @@ public function __construct() {
3248
* @param string $selected_sitegen_homepage The selected sitegen homepage to publish.
3349
* @return int|\WP_Error
3450
*/
35-
public function publish_homepage( string $selected_sitegen_homepage ): int | \WP_Error {
51+
public function publish_homepage( string $selected_sitegen_homepage ) {
3652
// Validate we have the selected homepage.
3753
if (
3854
! $this->sitegen_data ||
@@ -64,6 +80,9 @@ public function publish_homepage( string $selected_sitegen_homepage ): int | \WP
6480
);
6581
}
6682

83+
// Process images immediately in background (non-blocking)
84+
SiteGenImageService::process_homepage_images_immediate_async( $post_id, $content );
85+
6786
// Add the homepage to the site navigation.
6887
$this->add_page_to_navigation( $post_id, $title, get_permalink( $post_id ) );
6988

@@ -84,9 +103,9 @@ public function publish_homepage( string $selected_sitegen_homepage ): int | \WP
84103
* @param string $slug The slug of the page to get the title for.
85104
* @return string|false The page title, or false if not found.
86105
*/
87-
public function get_sitemap_page_title( string $slug ): string|false {
88-
$prompt = $this->get_prompt();
89-
$locale = $this->get_locale();
106+
public function get_sitemap_page_title( string $slug ) {
107+
$prompt = $this->get_prompt();
108+
$locale = $this->get_locale();
90109
$site_type = $this->get_site_type();
91110
if ( ! $prompt || ! $locale || ! $site_type ) {
92111
return false;
@@ -108,7 +127,7 @@ public function get_sitemap_page_title( string $slug ): string|false {
108127
/**
109128
* Add a page to the site navigation.
110129
*
111-
* @param int $post_id The ID of the page to add to the navigation.
130+
* @param int $post_id The ID of the page to add to the navigation.
112131
* @param string $page_title The title of the page.
113132
* @param string $permalink The permalink of the page.
114133
*/
@@ -140,7 +159,7 @@ public function add_page_to_navigation( int $post_id, string $page_title, string
140159
*
141160
* @return string|false
142161
*/
143-
public function get_prompt(): string|false {
162+
public function get_prompt() {
144163
return ! empty( $this->input_data['prompt'] ) ? $this->input_data['prompt'] : false;
145164
}
146165

@@ -149,7 +168,7 @@ public function get_prompt(): string|false {
149168
*
150169
* @return string
151170
*/
152-
public function get_site_type(): string {
171+
public function get_site_type() {
153172
return ! empty( $this->input_data['siteType'] ) ? $this->input_data['siteType'] : 'business';
154173
}
155174

@@ -158,7 +177,7 @@ public function get_site_type(): string {
158177
*
159178
* @return string
160179
*/
161-
public function get_locale(): string {
180+
public function get_locale() {
162181
return ! empty( $this->input_data['locale'] ) ? $this->input_data['locale'] : 'en_US';
163182
}
164183
}

0 commit comments

Comments
 (0)