PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / trunk
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! vtrunk
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 / Utils / Utils.php

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

674 lines 21.1 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\Utils;
4
5 use Exception;
6 use Templately\Modules\ThemeBuilder\PageTemplates;
7 use Templately\Modules\ThemeBuilder\Factory\TemplateFactory;
8 use Templately\Modules\FullSiteImport\FullSiteImport;
9 use Templately\Modules\FullSiteImport\WPImport;
10 use Templately\Utils\Base;
11 use Templately\Utils\Helper;
12 use Templately\Utils\Log\Logger;
13
14 class Utils extends Base {
15
16 /**
17 * Filter callback to prefer the GD image editor over Imagick during imports.
18 * Imagick can fail on certain server configurations; GD is more reliable for
19 * the resize/crop operations triggered by wp_generate_attachment_metadata().
20 */
21 public static function prefer_gd_editor( $editors ) {
22 if ( is_callable( [ 'WP_Image_Editor_GD', 'test' ] ) && call_user_func( [ 'WP_Image_Editor_GD', 'test' ] ) ) {
23 return [ 'WP_Image_Editor_GD' ];
24 }
25 return $editors;
26 }
27
28 public static function add_gd_editor_filter() {
29 if ( ! has_filter( 'wp_image_editors', [ self::class, 'prefer_gd_editor' ] ) ) {
30 add_filter( 'wp_image_editors', [ self::class, 'prefer_gd_editor' ] );
31 }
32 }
33
34 /**
35 * @throws Exception
36 */
37 public static function read_json_file( $path ) {
38 if ( ! file_exists( $path ) ) {
39 throw new Exception( __( 'JSON file not exists. ' . basename( $path ), 'templately' ) );
40 }
41
42 $file_content = self::file_get_contents( $path );
43
44 return $file_content ? json_decode( $file_content, true ) : [];
45 }
46
47 /**
48 * @param $file
49 * @param mixed ...$args
50 *
51 * @return false|string
52 */
53 public static function file_get_contents( $file, ...$args ) {
54 if ( ! is_file( $file ) || ! is_readable( $file ) ) {
55 return false;
56 }
57
58 return file_get_contents( $file, ...$args );
59 }
60
61 public static function get_builtin_wp_post_types(): array {
62 $post_type_args = [
63 'show_in_nav_menus' => true,
64 'public' => true
65 ];
66 $_post_types = get_post_types( $post_type_args, 'objects' );
67
68 return array_merge( array_keys( $_post_types ), [ 'nav_menu_item', 'wp_navigation' ] );
69 }
70
71 public static function map_old_new_post_ids( array $imported_data ) {
72 $result = [];
73
74 $result += $imported_data['templates']['succeed'] ?? [];
75
76 if ( isset( $imported_data['content'] ) ) {
77 foreach ( $imported_data['content'] as $post_type ) {
78 $result += $post_type['succeed'] ?? [];
79 }
80 }
81
82 if ( isset( $imported_data['wp-content'] ) ) {
83 foreach ( $imported_data['wp-content'] as $post_type ) {
84 $result += $post_type['succeed'] ?? [];
85 }
86 }
87
88 // add attachments data
89 if ( !empty( $imported_data['attachments']['succeed'] ) ) {
90 $result += $imported_data['attachments']['succeed'] ?? [];
91 }
92
93
94 return $result;
95 }
96
97 public static function map_old_new_term_ids( array $imported_data ) {
98 $result = [];
99
100 if ( isset( $imported_data['terms'] ) ) {
101 foreach ( $imported_data['terms'] as $post_type ) {
102 $result += $post_type['succeed'] ?? [];
103 }
104 }
105
106 return $result;
107 }
108
109 public static function map_old_new_term_ids_el( array $imported_data ): array {
110 $result = [];
111
112 if ( ! isset( $imported_data['taxonomies'] ) ) {
113 return $result;
114 }
115
116 foreach ( $imported_data['taxonomies'] as $post_type_taxonomies ) {
117 foreach ( $post_type_taxonomies as $taxonomy ) {
118 foreach ( $taxonomy as $term ) {
119 $result[ $term['old_id'] ] = $term['new_id'];
120 }
121 }
122 }
123
124 return $result;
125 }
126
127 /**
128 * Resolve the platform-specific import helper.
129 *
130 * Elementor is the in-module default (ElementorHelper lives here). Any other
131 * platform is resolved through the `templately_fsi_json_helper` filter seam so
132 * FSI does not import platform modules directly — gutenberg-integration hooks
133 * this to supply its own GutenbergHelper. A filter that yields nothing for a
134 * non-elementor platform is logged (a silent-wrong fallback must be loud) and
135 * the ElementorHelper is used as a last resort.
136 *
137 * @param string $platform
138 *
139 * @return ImportHelper
140 */
141 public static function get_json_helper( string $platform ) {
142 if ( $platform === 'elementor' ) {
143 return new ElementorHelper();
144 }
145
146 $helper = apply_filters( 'templately_fsi_json_helper', null, $platform );
147 if ( $helper instanceof ImportHelper ) {
148 return $helper;
149 }
150
151 // Routed through Logger (WP_DEBUG_LOG gated) rather than a bare error_log:
152 // this fires on every call for an unrecognised platform, so ungated it wrote
153 // to the production PHP error log on a hot path (PRD PHP-6).
154 Logger::warning( sprintf(
155 'No import json helper registered for platform "%s" (templately_fsi_json_helper filter yielded nothing); falling back to ElementorHelper.',
156 $platform
157 ), 'fsi.get_json_helper' );
158
159 return new ElementorHelper();
160 }
161
162 public static function get_backup_options() {
163 global $wpdb;
164
165 $prefix = '__templately_';
166 $table_name = $wpdb->options; // Assuming default options table name
167
168 $sql = "SELECT option_name, option_value FROM {$table_name} WHERE option_name LIKE %s";
169 $prepared_sql = $wpdb->prepare($sql, array("$prefix%")); // Escape wildcard for security
170
171 $results = $wpdb->get_results($prepared_sql);
172
173 $templately_options = array();
174 foreach ($results as $row) {
175 $name = str_replace($prefix, '', $row->option_name);
176 $templately_options[$name] = maybe_unserialize($row->option_value);
177 }
178
179 return $templately_options;
180 }
181
182 public static function backup_option_value($key, $autoload = 'no') {
183 $old_value = get_option($key);
184 if ($old_value) {
185 update_option("__templately_$key", $old_value, $autoload);
186 }
187 else {
188 add_option("__templately_$key", $old_value, '', $autoload);
189 }
190 }
191
192 public static function update_option($key, $value, $autoload = 'no') {
193 self::backup_option_value($key, $autoload);
194 return update_option($key, $value, $autoload);
195 }
196
197 /**
198 * Create the WordPress page an archive-type template attaches to, and wire up
199 * the matching WP/WooCommerce/LearnDash setting.
200 *
201 * Shared by the FSI Templates runner and single-template Library imports so both
202 * produce the same companion page + settings (page_for_posts,
203 * woocommerce_shop_page_id, LearnDash courses slug).
204 *
205 * @param array $template_settings Needs 'type'; optional 'title' and 'page_settings.archive_page_id'.
206 * @param string $platform 'elementor' | 'gutenberg'.
207 * @param array $manifest FSI manifest (optional). Only used to skip creation when the
208 * archive page is already part of the imported content.
209 *
210 * @return false|int New page ID, or false on skip/failure.
211 */
212 public static function create_archive_page( $template_settings, $platform, $manifest = [] ) {
213 try {
214 $archive_page_id = $template_settings['page_settings']['archive_page_id'] ?? null;
215 if ( $archive_page_id && ! empty( $manifest['content']['page'][ $archive_page_id ] ) ) {
216 return false;
217 }
218
219 $type = $template_settings['type'];
220
221 $archive_page = wp_insert_post( [
222 'post_title' => $template_settings['title'] ?? ucfirst( $type ) . ' - (by Templately)',
223 'post_status' => 'publish',
224 'post_type' => 'page',
225 'post_content' => '',
226 'page_template' => $platform === 'elementor' ? 'elementor_header_footer' : PageTemplates::TEMPLATE_HEADER_FOOTER,
227 ] );
228
229 if ( is_wp_error( $archive_page ) ) {
230 return false;
231 }
232
233 if ( $type === 'archive' ) {
234 self::update_option( 'page_for_posts', $archive_page );
235 }
236
237 if ( $type === 'product_archive' ) {
238 self::update_option( 'woocommerce_shop_page_id', $archive_page );
239 }
240
241 if ( $type === 'course_archive' ) {
242 // get page slug from $archive_page id and update learndash_settings_permalinks option to courses.
243 $post_name = get_post_field( 'post_name', $archive_page );
244
245 if ( class_exists( '\LearnDash_Settings_Section' ) ) {
246 $section = \LearnDash_Settings_Section::get_section_instance( 'LearnDash_Settings_Section_Permalinks' );
247 if ( ! empty( $section ) ) {
248 $section->set_setting( 'courses', $post_name );
249 if ( function_exists( 'learndash_setup_rewrite_flush' ) ) {
250 learndash_setup_rewrite_flush();
251 }
252 }
253 }
254 }
255
256 return $archive_page;
257 } catch ( \Exception $e ) {
258 return false;
259 }
260 }
261
262 /**
263 * Create a single-page Library template carrying a post-content widget/block,
264 * so a fluent_product_single import has a page template to render into.
265 *
266 * Shared by the FSI Templates runner and single-template Library imports.
267 *
268 * @param string $platform 'elementor' | 'gutenberg'.
269 *
270 * @return void
271 */
272 public static function create_page_template( $platform ) {
273 try {
274 $meta = [];
275 $data = [];
276
277 $post_data = [
278 'post_title' => 'Single Page - (by Templately)',
279 'post_status' => 'publish',
280 'post_type' => 'templately_library',
281 ];
282
283 if ( $platform === 'elementor' ) {
284 $meta['_wp_page_template'] = 'elementor_header_footer';
285 $data = json_decode( '{"content":[{"id":"4a86515d","settings":[],"elements":[{"id":"30a46db1","settings":{"content_width":"full"},"elements":[],"isInner":false,"widgetType":"tl-post-content","elType":"widget"}],"isInner":false,"elType":"container"}],"settings":{"template":"elementor_header_footer"},"metadata":[]}', true );
286 } elseif ( $platform === 'gutenberg' ) {
287 $meta['_wp_page_template'] = PageTemplates::TEMPLATE_HEADER_FOOTER;
288 $data = [ 'content' => '<!-- wp:post-content /-->' ];
289 }
290
291 $factory = new TemplateFactory( $platform );
292 $template = $factory->create( 'page_single', $post_data, $meta );
293 $template->import( $data );
294 } catch ( \Exception $e ) {
295 return;
296 }
297 }
298
299 /**
300 * Run the companion-content side effects for a freshly imported Library template.
301 *
302 * Dispatches by resolved Builder type:
303 * - archive / product_archive / course_archive → attached WP page + settings
304 * - fluent_product_single → single-page template
305 *
306 * Convenience wrapper for single-template imports, which have no FSI manifest.
307 *
308 * @param string $type Resolved Builder type key.
309 * @param string $title Template title (used for the created page).
310 * @param string $platform 'elementor' | 'gutenberg'.
311 *
312 * @return false|int|null Archive page ID for archive types, null otherwise.
313 */
314 public static function create_companion_content( $type, $title, $platform ) {
315 if ( in_array( $type, [ 'archive', 'product_archive', 'course_archive' ], true ) ) {
316 return self::create_archive_page( [ 'title' => $title, 'type' => $type ], $platform );
317 }
318
319 if ( $type === 'fluent_product_single' ) {
320 self::create_page_template( $platform );
321 }
322
323 return null;
324 }
325
326 public static function import_page_settings( $id, $settings ) {
327 $extra_settings = [
328 'page_on_front' => [
329 'show_on_front' => 'page'
330 ]
331 ];
332 if ( isset( $settings['page_for_posts'] ) && $settings['page_for_posts'] ) {
333 self::update_option( 'page_for_posts', $id );
334 }
335 if ( isset( $settings['show_on_front'] ) && $settings['show_on_front'] ) {
336 self::update_option( 'page_on_front', $id );
337 self::update_option( 'show_on_front', 'page' );
338 }
339 if ( ! empty( $settings['page_settings'] ) ) {
340 foreach ( $settings['page_settings'] as $option_name => $val ) {
341 $__val = $id;
342 if($option_name === 'fluent_cart_store_settings'){
343 $__val = $val;
344 }
345 self::update_option( $option_name, $__val );
346 if ( array_key_exists( $option_name, $extra_settings ) ) {
347 foreach ( $extra_settings[ $option_name ] as $name => $value ) {
348 self::update_option( $name, $value );
349 }
350 }
351 }
352 }
353 }
354
355 /**
356 * Get a remote logo URL from the supported import payload shapes.
357 *
358 * @param mixed $logo A URL string or an array containing a URL.
359 * @return string
360 */
361 public static function get_logo_url( $logo ) {
362 if ( is_string( $logo ) ) {
363 return $logo;
364 }
365
366 if ( is_array( $logo ) && isset( $logo['url'] ) && is_string( $logo['url'] ) ) {
367 return $logo['url'];
368 }
369
370 return '';
371 }
372
373 public static function upload_logo($url, $session_id) {
374 if(empty($url)) {
375 return ['error' => __('URL is empty', 'templately')];
376 }
377
378 // Validate URL and ensure scheme is present
379 if ( ! wp_http_validate_url( $url ) || !parse_url( $url, PHP_URL_SCHEME ) ) {
380 return ['error' => __('Invalid URL', 'templately')];
381 }
382
383 $post_data = self::prepare_post_data($url);
384 $wp_importer = new WPImport( null, ['fetch_attachments' => true, 'session_id' => $session_id] );
385 $attachment_id = $wp_importer->process_attachment($post_data, $url);
386
387 if(is_wp_error($attachment_id)){
388 return ['error' => $attachment_id->get_error_message()];
389 }
390
391 return [
392 'id' => (int) $attachment_id,
393 'url' => esc_url_raw(wp_get_attachment_url($attachment_id)),
394 ];
395 }
396
397 /**
398 * Upload base64 encoded image to media library
399 *
400 * @param string $base64 Base64 encoded image data (with or without data URI scheme)
401 * @param string $session_id Session ID for tracking (reserved for future use)
402 * @return array Array with 'id' and 'url' on success, or ['error' => message] on failure
403 */
404 public static function upload_logo_base64($base64, $session_id = null) {
405 if(empty($base64)) {
406 return ['error' => __('Base64 is empty', 'templately')];
407 }
408
409 // Upload the base64 image
410 $attachment_id = self::upload_base64_image($base64);
411
412 if(is_wp_error($attachment_id)){
413 return ['error' => $attachment_id->get_error_message()];
414 }
415
416 return [
417 'id' => (int) $attachment_id,
418 'url' => esc_url_raw(wp_get_attachment_url($attachment_id)),
419 ];
420 }
421
422 /**
423 * Upload base64 encoded image without dependency on prepare_post_data
424 * Handles MIME type detection and proper file extension assignment
425 *
426 * @param string $base64 Base64 encoded image data (with or without data URI scheme)
427 * @return int|\WP_Error Attachment ID on success, WP_Error on failure
428 */
429 public static function upload_base64_image($base64) {
430 // Strip data URL prefix if present (e.g., "data:image/png;base64,")
431 if (strpos($base64, 'data:image/') === 0) {
432 $base64_parts = explode(',', $base64, 2);
433 if (isset($base64_parts[1])) {
434 $base64 = $base64_parts[1];
435 }
436 }
437
438 // Decode base64 string
439 $decoded_image = base64_decode($base64, true);
440 if ($decoded_image === false) {
441 return new \WP_Error('invalid_base64', __('Invalid base64 data.', 'templately'));
442 }
443
444 // Detect MIME type from decoded image data
445 $mime_type = self::detect_mime_type_from_data($decoded_image);
446 if (empty($mime_type)) {
447 return new \WP_Error('unknown_mime_type', __('Unable to determine image MIME type.', 'templately'));
448 }
449
450 // Get file extension from MIME type
451 $extension = self::get_file_extension_by_mime_type($mime_type);
452 if (empty($extension)) {
453 return new \WP_Error('unsupported_mime_type', __('Unsupported image MIME type.', 'templately'));
454 }
455
456 // Generate unique filename
457 $filename = 'templately-logo-' . \wp_generate_uuid4() . '.' . $extension;
458
459 // Get upload directory
460 $upload_dir = \wp_upload_dir();
461 if (!$upload_dir['error']) {
462 $upload_path = $upload_dir['path'] . '/' . $filename;
463 } else {
464 return new \WP_Error('upload_dir_error', __('Unable to access upload directory.', 'templately'));
465 }
466
467 // Write decoded image to file
468 if (file_put_contents($upload_path, $decoded_image) === false) {
469 return new \WP_Error('upload_error', __('Error uploading image.', 'templately'));
470 }
471
472 // Create attachment post
473 $attachment_data = [
474 'post_mime_type' => $mime_type,
475 'post_title' => \sanitize_file_name(pathinfo($filename, PATHINFO_FILENAME)),
476 'post_content' => '',
477 'post_status' => 'inherit',
478 ];
479
480 $attachment_id = \wp_insert_attachment($attachment_data, $upload_path);
481 if (is_wp_error($attachment_id)) {
482 return $attachment_id;
483 }
484
485 // Ensure WordPress image functions are available
486 // These functions are defined in wp-admin/includes/image.php which is not always loaded
487 if (!function_exists('wp_generate_attachment_metadata')) {
488 require_once(ABSPATH . 'wp-admin/includes/image.php');
489 }
490
491 // Generate and update attachment metadata
492 $metadata = \wp_generate_attachment_metadata($attachment_id, $upload_path);
493 \wp_update_attachment_metadata($attachment_id, $metadata);
494
495 return $attachment_id;
496 }
497
498 /**
499 * Detect MIME type from image data
500 * Uses finfo_buffer if available, otherwise falls back to getimagesizefromstring
501 *
502 * @param string $image_data Raw image data
503 * @return string|null MIME type or null if unable to detect
504 */
505 private static function detect_mime_type_from_data($image_data) {
506 // Try using finfo_buffer first (most reliable)
507 if (function_exists('finfo_buffer')) {
508 $finfo = finfo_open(FILEINFO_MIME_TYPE);
509 if ($finfo) {
510 $mime_type = finfo_buffer($finfo, $image_data);
511 finfo_close($finfo);
512 if ($mime_type && strpos($mime_type, 'image/') === 0) {
513 return $mime_type;
514 }
515 }
516 }
517
518 // Fallback: use getimagesizefromstring
519 if (function_exists('getimagesizefromstring')) {
520 $image_info = @getimagesizefromstring($image_data);
521 if ($image_info && isset($image_info['mime'])) {
522 return $image_info['mime'];
523 }
524 }
525
526 return null;
527 }
528
529 /**
530 * Get file extension by MIME type
531 * Uses WordPress built-in functions for MIME type to extension conversion
532 *
533 * @since 3.4.5
534 * @param string $mime_type MIME type (e.g., 'image/png')
535 * @return string|null File extension without dot, or null if not found
536 */
537 private static function get_file_extension_by_mime_type($mime_type) {
538 // Use WordPress core function if available (WordPress 5.8.1+)
539 // wp_get_default_extension_for_mime_type() returns the default file extension for a given MIME type
540 if (function_exists('wp_get_default_extension_for_mime_type')) {
541 return \wp_get_default_extension_for_mime_type($mime_type);
542 }
543
544 // Fallback for WordPress < 5.8.1
545 // Use wp_get_mime_types() which returns array with extensions as keys and MIME types as values
546 // Example: ['jpg|jpeg|jpe' => 'image/jpeg', 'png' => 'image/png', ...]
547 $wp_mime_types = \wp_get_mime_types();
548
549 // Flip the array to get MIME type as key and extensions as value
550 $mime_map = array_flip($wp_mime_types);
551
552 if (isset($mime_map[$mime_type])) {
553 $extensions = $mime_map[$mime_type];
554 // Get first extension if multiple are available (e.g., 'jpg|jpeg|jpe' -> 'jpg')
555 return strtok($extensions, '|');
556 }
557
558 return null;
559 }
560
561 /**
562 * Inserts a template into the Gutenberg editor.
563 *
564 * @param mixed $data
565 * @param int $postId
566 * @return array
567 */
568 public static function import_and_replace_attachments($content, $postId = 0, $attachment_options = []) {
569 // Resolve the Gutenberg import helper via the platform filter seam.
570 $helper = self::get_json_helper( 'gutenberg' );
571
572 // get_json_helper() falls back to ElementorHelper when no gutenberg helper is
573 // registered, and set_attachment_options() only exists on GutenbergHelper.
574 if ( ! empty( $attachment_options ) && method_exists( $helper, 'set_attachment_options' ) ) {
575 $helper->set_attachment_options( $attachment_options );
576 }
577
578 if ( ! empty( $attachment_options ) ) {
579 $helper->set_attachment_options( $attachment_options );
580 }
581
582 $data = [
583 'content' => $content,
584 ];
585
586 // Organize URLs from the content
587 $organizedUrls = $helper->parse_images($data['content']);
588 if(empty($organizedUrls)){
589 return $content;
590 }
591
592 // Define template settings
593 $template_settings = [
594 'post_id' => $postId,
595 '__attachments' => $organizedUrls,
596 ];
597
598 // Map post IDs and disable logging
599 $helper->map_post_ids[$postId] = $postId;
600 $helper->shouldLog = false;
601
602 // Prepare the helper with the data and settings
603 $helper->prepare($data, $template_settings);
604
605 // Update the content in the data array
606 $content = wp_unslash($helper->get_content());
607
608 return $content;
609 }
610
611 public static function prepare_post_data($image_url, $post_parent = null, $logger = null) {
612 $filetype = wp_check_filetype(basename($image_url));
613 if (!$filetype['type']) {
614 if(is_callable($logger)){
615 // call the logger function
616 call_user_func($logger, 'prepare', 'Error: Unable to determine the file type.', -1, 'eventLog');
617 }
618 return null;
619 }
620
621 $post_data = array(
622 'post_title' => basename($image_url),
623 'post_content' => '',
624 'post_status' => 'inherit',
625 'post_mime_type' => $filetype['type'],
626 'guid' => $image_url,
627 );
628
629 if($post_parent){
630 $post_data['post_parent'] = $post_parent; // Set the parent post
631 }
632
633 if (preg_match('%wp-content/uploads/([0-9]{4}/[0-9]{2})%', $image_url, $matches)) {
634 $post_data['upload_date'] = $matches[1];
635 }
636 else{
637 $post_data['upload_date'] = date('Y/m');
638 }
639
640 return $post_data;
641 }
642
643 /**
644 * Clean up directory using RecursiveIteratorIterator approach
645 * This method handles directory cleanup with proper validation
646 *
647 * @param string $dir_path The directory path to clean up
648 * @return bool True on success, false on failure
649 */
650 public static function cleanup_directory($dir_path) {
651 if (empty($dir_path) || !file_exists($dir_path) || !is_dir($dir_path)) {
652 return false;
653 }
654
655 try {
656 $files = new \RecursiveIteratorIterator(
657 new \RecursiveDirectoryIterator($dir_path, \RecursiveDirectoryIterator::SKIP_DOTS),
658 \RecursiveIteratorIterator::CHILD_FIRST
659 );
660
661 foreach ($files as $fileinfo) {
662 $todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
663 $todo($fileinfo->getRealPath());
664 }
665
666 rmdir($dir_path);
667 return true;
668 } catch (Exception $e) {
669 return false;
670 }
671 }
672
673 }
674