PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.8.0
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.8.0
3.8.0 3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 All 112 releases
templately / modules / full-site-import / Runners / Finalizer.php

Finalizer.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! 3.8.0, at modules/full-site-import/Runners/Finalizer.php

460 lines 15.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Templately\Modules\FullSiteImport\Runners;
4
5
6 use Exception;
7 use Templately\Modules\AiContentMerger\Utils\AIContentHelper;
8 use Templately\Modules\FullSiteImport\Utils\Utils;
9 use Templately\Modules\FullSiteImport\Utils\AIUtils;
10 use Templately\Modules\FullSiteImport\Utils\AIContentResolver;
11 use Templately\Modules\FullSiteImport\Utils\SessionData;
12 use Templately\Utils\Helper;
13
14 class Finalizer extends BaseRunner {
15 private $options = [];
16 private $type_to_check = [ 'templates', 'content' ];
17 private $imported_data;
18
19 public $type = '';
20 private $data = [];
21 public $sub_type = '';
22 private $extra_content;
23
24 private $total_counts = 0;
25
26 /**
27 * @var array|mixed
28 */
29 protected $map_post_ids = [];
30 /**
31 * @var array|mixed
32 */
33 protected $map_term_ids = [];
34
35 public function get_name(): string {
36 return 'finalize';
37 }
38
39 public function get_label(): string {
40 return __( 'Finalizing Your Imports', 'templately' );
41 }
42
43 public function log_message(): string {
44 return __( 'Finalizing Your Imports', 'templately' );
45 }
46
47 public function should_run( $param_data, $imported_data = [] ): bool {
48 $data = [];
49
50 foreach ( $this->type_to_check as $type ) {
51 $contents = ! empty ( $this->manifest[ $type ] ) ? $this->manifest[ $type ] : [];
52 if ( $type == 'templates' ) {
53 // $imported_data["templates"]["__attachments"][691]
54 $this->prepare( $data, $contents, $type, null, $imported_data );
55 } else {
56 foreach ( $contents as $post_type => $templates ) {
57 // $imported_data["content"]["__attachments"][$post_type][11]
58 $this->prepare( $data, $templates, $type, $post_type, $imported_data );
59 }
60 }
61 }
62 $this->options = &$data;
63
64 return ! empty( $data ) || $this->platform == 'gutenberg';
65 }
66
67 private function prepare( &$data, $templates, $type, $sub_type = null, $imported_data = [] ) {
68 if ( empty( $templates ) || ! is_array( $templates ) ) {
69 return;
70 }
71 foreach ( $templates as $id => $template ) {
72 // Check for __attachments in both template and imported_data
73 $has_attachment = !empty($template['__attachments']);
74
75 if (!$has_attachment && !empty($imported_data)) {
76 if ($sub_type) {
77 // $imported_data["content"]["__attachments"]["page"][11]
78 $has_attachment = isset($imported_data[$type]['__attachments'][$sub_type][$id]);
79 } else {
80 // $imported_data["templates"]["__attachments"][691]
81 $has_attachment = isset($imported_data[$type]['__attachments'][$id]);
82 }
83 }
84
85 // Pick up docs the import runners flagged as carrying source-site links
86 // (Mega Menu / nav / button URLs) so the finalize loop rewrites them to the
87 // real imported permalinks — same opt-in mechanism as __attachments above.
88 // $imported_data["templates"]["__link_rewrites"][52]
89 // $imported_data["content"]["page"]["__link_rewrites"][35]
90 $needs_link_rewrite = $sub_type
91 ? isset( $imported_data[ $type ][ $sub_type ]['__link_rewrites'][ $id ] )
92 : isset( $imported_data[ $type ]['__link_rewrites'][ $id ] );
93
94 if ( ! isset( $template['data'] ) && !$has_attachment && !$needs_link_rewrite && !isset($template['has_logo']) && !$this->is_ai_content($id) && !isset($template["page_settings"]["fluent_cart_store_settings"]) ) {
95 continue;
96 }
97
98 // if ( ! isset( $template['data']['form'] ) && ! isset( $template['data']['nav_menus'] )) {
99 // continue;
100 // }
101
102 $this->total_counts += 1;
103
104 if ( $sub_type ) {
105 $data[ $type ][ $sub_type ][ $id ] = $template;
106 } else {
107 $data[ $type ][ $id ] = $template;
108 }
109 }
110 }
111
112 public function import( $data, $imported_data ): array {
113 $this->data = &$data;
114 $this->imported_data = &$imported_data;
115
116 $this->json->imported_data = $this->imported_data;
117 $this->json->map_post_ids = Utils::map_old_new_post_ids( $this->imported_data );
118 $this->json->map_term_ids = Utils::map_old_new_term_ids( $this->imported_data );
119 if ( $this->manifest['platform'] === 'elementor' ) {
120 $this->json->set_source_url( $this->manifest['site'] ?? '' );
121 $this->json->set_source_pages( $this->build_source_pages_map() );
122 }
123 if ( ! empty( $imported_data['extra-content'] ) ) {
124 $this->extra_content = $imported_data['extra-content'];
125 }
126
127 add_action('templately_import.finalize_gutenberg_attachment', [$this, 'post_log'], 10, 2);
128
129 $this->update_settings();
130
131 $this->loop( $this->options, function($type, $contents ) {
132 $this->type = $type;
133
134 if ( $type == 'templates' ) {
135 $this->finalize_imports( $contents, $type );
136 } else {
137 $this->loop( $contents, function($post_type, $templates ) use($type) {
138 $this->sub_type = $post_type;
139 $this->finalize_imports( $templates, $type, $post_type );
140 }, $type);
141 }
142 });
143
144 if ( $this->platform == 'gutenberg' ) {
145 $this->regenerate_assets();
146 }
147
148 return [];
149 }
150
151 /**
152 * Build the ORIGINAL-slug => imported-permalink map handed to ElementorHelper so
153 * links can resolve to the page WordPress actually created (slug-change-proof).
154 *
155 * Keyed by the pack's original slug — which is what stored links still carry —
156 * with the value being get_permalink() of the imported post, so a conflict rename
157 * (about-us → about-us-2) is transparently followed. Pages win over other post
158 * types on a slug collision. WooCommerce system pages (shop, cart, checkout,
159 * my-account) are added as aliases since they live outside the manifest content.
160 *
161 * @return array
162 */
163 private function build_source_pages_map(): array {
164 $map = [];
165 $map_post_ids = $this->json->map_post_ids;
166 $content = $this->manifest['content'] ?? [];
167
168 if ( is_array( $content ) ) {
169 foreach ( $content as $post_type => $items ) {
170 if ( ! is_array( $items ) ) {
171 continue;
172 }
173
174 foreach ( $items as $old_id => $entry ) {
175 $slug = isset( $entry['slug'] ) ? strtolower( trim( (string) $entry['slug'] ) ) : '';
176 if ( '' === $slug ) {
177 continue;
178 }
179
180 $new_id = $map_post_ids[ $old_id ] ?? null;
181 if ( empty( $new_id ) ) {
182 continue;
183 }
184
185 $permalink = get_permalink( $new_id );
186 if ( ! $permalink || is_wp_error( $permalink ) ) {
187 continue;
188 }
189
190 // First match wins, but a real `page` always overrides another
191 // post type that happens to share the slug.
192 if ( ! isset( $map[ $slug ] ) || 'page' === $post_type ) {
193 $map[ $slug ] = $permalink;
194 }
195 }
196 }
197 }
198
199 $this->add_woocommerce_page_aliases( $map );
200
201 return $map;
202 }
203
204 /**
205 * Add WooCommerce system-page slugs to the resolution map. These pages are not in
206 * the manifest content, so menu links to them (often relative, e.g. /demo/shop,
207 * /demo/sign-in) would otherwise only get the plain domain swap.
208 *
209 * Heuristic: the demo's my-account page may be titled/slugged "sign-in", "login",
210 * etc.; those common aliases are mapped to this site's WooCommerce my-account page.
211 * Manifest pages already in the map are never overwritten.
212 *
213 * @param array $map slug => permalink (by reference).
214 *
215 * @return void
216 */
217 private function add_woocommerce_page_aliases( array &$map ) {
218 if ( ! function_exists( 'wc_get_page_permalink' ) ) {
219 return;
220 }
221
222 $aliases = [
223 'shop' => [ 'shop', 'store', 'products' ],
224 'cart' => [ 'cart' ],
225 'checkout' => [ 'checkout' ],
226 'myaccount' => [ 'my-account', 'myaccount', 'account', 'sign-in', 'signin', 'login', 'log-in', 'dashboard' ],
227 ];
228
229 foreach ( $aliases as $wc_page => $slugs ) {
230 $permalink = wc_get_page_permalink( $wc_page );
231 if ( ! $permalink ) {
232 continue;
233 }
234
235 foreach ( $slugs as $slug ) {
236 if ( ! isset( $map[ $slug ] ) ) {
237 $map[ $slug ] = $permalink;
238 }
239 }
240 }
241 }
242
243 private function regenerate_assets() {
244 $upload_dir = wp_upload_dir();
245 if ( is_dir( $upload_dir['basedir'] . '/eb-style/' ) ) {
246 array_map( 'unlink', glob( $upload_dir['basedir'] . '/eb-style/*.min.css' ) );
247 rmdir( $upload_dir['basedir'] . '/eb-style/' );
248 }
249 }
250
251 private function finalize_imports( $templates, $type, $post_type = null ) {
252 // used for counting
253 $processed = $this->get_loop_result([], 'finalized_imports', false);
254 $path = $this->dir_path . $this->type . DIRECTORY_SEPARATOR;
255
256 if ( ! empty( $this->sub_type ) ) {
257 $path .= $this->sub_type . DIRECTORY_SEPARATOR;
258 }
259
260 $this->loop( $templates, function($old_template_id, $template_settings ) use($type, $post_type, $path, $processed) {
261 try {
262
263 if($post_type && isset($this->imported_data[$type]['__attachments'][$post_type][$old_template_id])){
264 $template_settings['__attachments'] = $this->imported_data[$type]['__attachments'][$post_type][$old_template_id];
265 }
266 else if(empty($post_type) && isset($this->imported_data[$type]['__attachments'][$old_template_id])){
267 $template_settings['__attachments'] = $this->imported_data[$type]['__attachments'][$old_template_id];
268 }
269
270 // Read the original template file for non-AI content
271 $original_file = $path . "{$old_template_id}.json";
272 $template_json = Utils::read_json_file($original_file);
273
274 $updated_ids = AIUtils::get_processed_pages_data($this->process_id);
275 $ai_paths = AIUtils::generate_ai_file_paths(
276 $this->session_id,
277 $this->type,
278 $this->sub_type,
279 $old_template_id,
280 $this->dir_path
281 );
282 if($this->is_ai_content($old_template_id) && !file_exists($ai_paths['ai_file_path'])){
283 // Source-agnostic seam: whichever AI-content source owns this
284 // process (classic per-page callback, chat-id pull, or a future
285 // source) stages the .ai.json; otherwise the shared SSE-wait runs.
286 AIContentResolver::ensure_page_ready([
287 'session_id' => $this->session_id,
288 'process_id' => $this->process_id,
289 'content_id' => $old_template_id,
290 'ai_page_ids' => $this->ai_page_ids,
291 'updated_ids' => $updated_ids,
292 'sse_callback' => [$this, 'sse_message'],
293 'progress_id' => 'ai_content_time',
294 'additional_sse_data' => [
295 'name' => method_exists($this, 'get_name') ? $this->get_name() : '',
296 'post_type' => $post_type,
297 'id' => $old_template_id,
298 ],
299 ]);
300 }
301 // Check if this is AI content, then merge the AI-generated content into the
302 // original template. Session/path resolution + file IO live here (the
303 // consumer); the pure platform merge is delegated to ai-content-merger's
304 // standalone AIContentHelper::merge().
305 if ( AIUtils::should_process_as_ai_content( $this->session_id, $this->type, $this->sub_type, $old_template_id, $this->ai_page_ids, $this->dir_path ) ) {
306 $ai_file = $ai_paths['ai_file_path'];
307 if ( AIUtils::has_ai_file( $ai_file ) && ! AIUtils::is_ai_file_skipped( $ai_file ) ) {
308 $ai_json = Utils::read_json_file( $ai_file );
309 $original_json = Utils::read_json_file( $ai_paths['original_file'] );
310 $merged = AIContentHelper::merge( $ai_json, $original_json, $this->platform );
311
312 // A merge that could not keep a block's attributes and its markup in step
313 // leaves a block the editor reports as "unexpected or invalid content" and
314 // a front end still showing the pre-AI copy. That is invisible from the
315 // import log unless we say so, and it is the signal the post-import block
316 // rebuild pass uses to decide which posts are worth revisiting.
317 $parity_warnings = AIContentHelper::collect_parity_warnings();
318 if ( ! empty( $parity_warnings ) ) {
319 $this->record_parity_warnings( $old_template_id, $post_type, $parity_warnings );
320 }
321
322 // Debug file records the MERGED value pre-adoption — byte-faithful with
323 // the old AIContentHelper::processAiContent().
324 $this->write_ai_debug_file( $ai_paths['original_file'], $merged );
325
326 if ( ! empty( $merged ) ) {
327 $template_json = $merged;
328 }
329 }
330 }
331
332 $params = $this->origin->get_request_params();
333 $this->json->prepare( $template_json, $template_settings, $this->extra_content['form'][ $old_template_id ] ?? [], $params )->update();
334
335 $processed[] = $old_template_id;
336 // Add the template to the processed templates and update the session data
337 $this->set_loop_result( $processed, 'finalized_imports');
338 // Broadcast Log
339 $progress = floor( ( 100 * count($processed) ) / $this->total_counts );
340 if(empty($progress)){
341 $xyz = 0;
342 }
343 $this->log( $progress );
344
345 } catch ( Exception $e ) {
346
347 }
348
349
350 }, "$type-$post_type");
351 }
352
353 /**
354 * Persist + broadcast the attribute/markup parity failures from one AI merge.
355 *
356 * Stored on the session (not only logged) because the consumer is the post-import block
357 * rebuild pass, which runs in the BROWSER after this request is long gone and needs to know
358 * which imported posts are worth opening. Kept deliberately non-fatal: a desynced block
359 * still renders, and failing an otherwise complete import over it would be worse than the
360 * banner it produces.
361 *
362 * @param int|string $template_id Source template id (pack-side).
363 * @param string $post_type Post type being finalized.
364 * @param array $warnings From `AIContentHelper::collect_parity_warnings()`.
365 */
366 private function record_parity_warnings( $template_id, $post_type, array $warnings ) {
367 $new_post_id = $this->map_post_ids[ $template_id ] ?? null;
368
369 $existing = SessionData::get( $this->session_id, 'parity_warnings' );
370 $existing = is_array( $existing ) ? $existing : [];
371
372 $existing[] = [
373 'template_id' => $template_id,
374 'post_id' => $new_post_id,
375 'post_type' => $post_type,
376 'warnings' => $warnings,
377 ];
378
379 SessionData::set( $this->session_id, 'parity_warnings', $existing );
380
381 $blocks = [];
382 foreach ( $warnings as $warning ) {
383 $blocks[] = $warning['blockName'] . ( $warning['attribute'] ? ' (' . $warning['attribute'] . ')' : '' );
384 }
385
386 $this->sse_message( [
387 'type' => 'eventLog',
388 'action' => 'eventLog',
389 'info' => 'ai_content_parity',
390 'results' => sprintf(
391 /* translators: 1: number of blocks, 2: post type, 3: comma-separated block names */
392 __( '%1$d block(s) in this %2$s kept their original text in the markup: %3$s', 'templately' ),
393 count( $warnings ),
394 $post_type,
395 implode( ', ', array_unique( $blocks ) )
396 ),
397 ] );
398
399 Helper::log(
400 [
401 'context' => 'ai_content_parity',
402 'post_id' => $new_post_id,
403 'warnings' => $warnings,
404 ],
405 'fsi_event'
406 );
407 }
408
409 public function post_log($id, $size_dimension = null){
410 $this->log(-1, "Imported attachment: $id" . ( $size_dimension ? " - $size_dimension" : ''), 'eventLog');
411 }
412
413 /**
414 * Write the merged AI content to a sibling debug file (.ao.json) when a dev/debug
415 * constant is set. Relocated verbatim from AIContentHelper::writeDebugFile() when that
416 * trait became a standalone class (spec 022 FR-011) — the session/file-IO orchestration
417 * that surrounds the pure merge is the consumer's responsibility.
418 */
419 private function write_ai_debug_file( $original_file, $data, $type = 'ao' ) {
420 if ((defined('TEMPLATELY_DEV') && TEMPLATELY_DEV) || (defined('IMPORT_DEBUG') && IMPORT_DEBUG)) {
421 $replace = ".{$type}.json";
422 $debug_file = str_replace('.json', $replace, $original_file);
423 file_put_contents($debug_file, json_encode($data));
424 }
425 }
426
427 private function update_settings() {
428 $data = $this->data;
429 $map_post_ids = $this->json->map_post_ids;
430 $saved_options = get_option('fluent_cart_store_settings', []);
431 $mapped_keys = [
432 'checkout_page_id',
433 'custom_payment_page_id',
434 'registration_page_id',
435 'login_page_id',
436 'cart_page_id',
437 'receipt_page_id',
438 'shop_page_id',
439 'customer_profile_page_id',
440 ];
441
442 foreach ($mapped_keys as $key) {
443 $old_id = $saved_options[$key] ?? null;
444 if (!empty($old_id) && isset($map_post_ids[$old_id])) {
445 $saved_options[$key] = $map_post_ids[$old_id];
446 }
447 else if(isset($saved_options[$key])){
448 unset($saved_options[$key]);
449 }
450 }
451
452 update_option('fluent_cart_store_settings', $saved_options);
453 }
454
455
456
457
458
459 }
460