Skip to content

Commit d8dc6e0

Browse files
committed
Merge branch 'add/site-type' into add/sitekits
2 parents 7e3239e + 0c8cd20 commit d8dc6e0

File tree

5 files changed

+583
-13
lines changed

5 files changed

+583
-13
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: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,18 @@
99
use NewfoldLabs\WP\Module\Onboarding\Types\Color;
1010
use NewfoldLabs\WP\Module\Onboarding\Types\ColorPalette;
1111
use NewfoldLabs\WP\Module\Onboarding\Types\SiteClassification;
12-
12+
use NewfoldLabs\WP\Module\Onboarding\Services\SiteGenImageService;
13+
use NewfoldLabs\WP\Module\Onboarding\Services\ReduxStateService;
14+
15+
/**
16+
* Class SiteGenService
17+
*
18+
* Handles the onboarding SiteGen flow for generating, publishing, and managing AI-generated homepages and related content.
19+
*
20+
* This service is designed to work with the onboarding module's Redux state and integrates with other onboarding services.
21+
*
22+
* @package NewfoldLabs\WP\Module\Onboarding\Services
23+
*/
1324
class SiteGenService {
1425

1526
/**
@@ -24,17 +35,19 @@ class SiteGenService {
2435
*
2536
* @var array|null
2637
*/
27-
private ?array $input_data = null;
28-
38+
private $input_data = null;
39+
2940
/**
3041
* The Redux sitegen object.
3142
*
3243
* @var array|null
3344
*/
34-
private ?array $sitegen_data = null;
45+
private $sitegen_data = null;
3546

3647
/**
37-
* Constructor.
48+
* SiteGenService constructor.
49+
*
50+
* Initializes the service by loading the Redux input and sitegen data from the ReduxStateService.
3851
*/
3952
public function __construct() {
4053
$this->input_data = ReduxStateService::get( 'input' );
@@ -92,7 +105,7 @@ public function get_sitekits( string $site_description, string $site_type, strin
92105
* @param string $selected_sitegen_homepage The selected sitegen homepage to publish.
93106
* @return int|\WP_Error
94107
*/
95-
public function publish_homepage( string $selected_sitegen_homepage ): int | \WP_Error {
108+
public function publish_homepage( string $selected_sitegen_homepage ) {
96109
// Validate we have the selected homepage.
97110
if (
98111
! $this->sitegen_data ||
@@ -124,6 +137,9 @@ public function publish_homepage( string $selected_sitegen_homepage ): int | \WP
124137
);
125138
}
126139

140+
// Process images immediately in background (non-blocking)
141+
SiteGenImageService::process_homepage_images_immediate_async( $post_id, $content );
142+
127143
// Add the homepage to the site navigation.
128144
$this->add_page_to_navigation( $post_id, $title, get_permalink( $post_id ) );
129145

@@ -144,9 +160,9 @@ public function publish_homepage( string $selected_sitegen_homepage ): int | \WP
144160
* @param string $slug The slug of the page to get the title for.
145161
* @return string|false The page title, or false if not found.
146162
*/
147-
public function get_sitemap_page_title( string $slug ): string|false {
148-
$prompt = $this->get_prompt();
149-
$locale = $this->get_locale();
163+
public function get_sitemap_page_title( string $slug ) {
164+
$prompt = $this->get_prompt();
165+
$locale = $this->get_locale();
150166
$site_type = $this->get_site_type();
151167
if ( ! $prompt || ! $locale || ! $site_type ) {
152168
return false;
@@ -227,7 +243,7 @@ public function get_color_palette() {
227243
/**
228244
* Add a page to the site navigation.
229245
*
230-
* @param int $post_id The ID of the page to add to the navigation.
246+
* @param int $post_id The ID of the page to add to the navigation.
231247
* @param string $page_title The title of the page.
232248
* @param string $permalink The permalink of the page.
233249
*/
@@ -259,7 +275,7 @@ public function add_page_to_navigation( int $post_id, string $page_title, string
259275
*
260276
* @return string|false
261277
*/
262-
public function get_prompt(): string|false {
278+
public function get_prompt() {
263279
return ! empty( $this->input_data['prompt'] ) ? $this->input_data['prompt'] : false;
264280
}
265281

@@ -268,7 +284,7 @@ public function get_prompt(): string|false {
268284
*
269285
* @return string
270286
*/
271-
public function get_site_type(): string {
287+
public function get_site_type() {
272288
return ! empty( $this->input_data['siteType'] ) ? $this->input_data['siteType'] : 'business';
273289
}
274290

@@ -277,7 +293,7 @@ public function get_site_type(): string {
277293
*
278294
* @return string
279295
*/
280-
public function get_locale(): string {
296+
public function get_locale() {
281297
return ! empty( $this->input_data['locale'] ) ? $this->input_data['locale'] : 'en_US';
282298
}
283299
}

0 commit comments

Comments
 (0)