PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.12.13
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.12.13
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
wpfunnels / includes / core / import-export / class-wpfnl-import-funnel.php

class-wpfnl-import-funnel.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.12.13, at includes/core/import-export/class-wpfnl-import-funnel.php

826 lines 36.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPFunnels\Import;
4 use WPFunnels\Wpfnl_functions;
5 use WPFunnels\Wpfnl;
6 use WPFunnels\Data_Store\Wpfnl_Steps_Store_Data;
7 use lementor\TemplateLibrary\Source_Base;
8 use WPFunnelsPro\AbTesting\Wpfnl_Ab_Testing;
9 use Elementor\Plugin;
10 use WPFunnels\Admin\Notices\Notice as Admin_Notice;
11
12 /**
13 * This class is responsible import funnel
14 *
15 * Import json file
16 * @since 1.7.5
17 */
18 class Wpfnl_Import {
19
20 /**
21 * Whether an Elementor step was imported and the file cache still needs flushing.
22 *
23 * @var bool
24 * @since 2.5.15
25 */
26 private $needs_elementor_cache_flush = false;
27
28 /**
29 * Initialize ajax callback
30 *
31 * @since 1.7.5
32 */
33 public function __construct() {
34 add_action( 'wp_ajax_wpfnl_import_funnels', [ $this, 'schedule_import_funnels' ] );
35 add_action( 'wp_ajax_nopriv_wpfnl_import_funnels', [ $this, 'schedule_import_funnels' ] );
36 add_action( 'wp_ajax_wpfnl_hide_import_funnel_notice', [ $this, 'hide_import_funnel_notice' ] );
37 add_action( 'wp_ajax_nopriv_wpfnl_hide_import_funnel_notice', [ $this, 'hide_import_funnel_notice' ] );
38 add_action( 'wpfnl_import_funnels', [ $this, 'import_funnels' ] );
39 add_filter( 'wpfnl_notices', [ $this, 'add_funnel_notice' ] );
40 }
41
42 /**
43 * Adds a custom admin notice to the WordPress admin dashboard.
44 *
45 * This function is a callback used to add a custom notice to the admin dashboard.
46 * The notice is added to the existing array of notices.
47 *
48 * @param array $notices The array of existing admin notices.
49 *
50 * @return array The updated array of admin notices with the added custom notice.
51 * @since 1.9.7
52 */
53 public function add_funnel_notice( $notices ) {
54 $notices[ 'WPFunnels\Import\Wpfnl_Import' ]= 'funnel_import_notice';
55 return $notices;
56 }
57
58 /**
59 * Displays a notice related to funnel import status.
60 *
61 * This static method is used to display a notice related to the funnel import status in the WordPress admin area.
62 * It checks whether the import status option is not empty and whether the current screen context allows displaying
63 * the notice. If the conditions are met, a notice is generated using the provided options and displayed in the admin area.
64 * The notice can be dismissed by the user.
65 *
66 * @since 1.9.7
67 */
68 public static function funnel_import_notice() {
69 $import_status = get_option( 'wpfnl_import_notice', '' );
70 if( !empty( $import_status ) ) {
71 global $current_screen;
72 $disabled_notice_page = [ 'wp_funnels' ];
73 if( 'plugins' === $current_screen->parent_base || !isset( $_GET[ 'page' ] ) || ( isset( $_GET[ 'page' ] ) && in_array( $_GET[ 'page' ], $disabled_notice_page ) ) ) {
74 $nonce_field = "<input type='hidden' id='wpfnl-import-nonce' value='" . esc_attr( wp_create_nonce('wpfnl-admin') ) . "'>";
75 $options = array(
76 'id' => 'wpfnl-import-notice',
77 'title' => 'wpfunnels-basic',
78 'description' => "<span>{$import_status}</span>{$nonce_field}",
79 'classes' => [ 'notice', 'notice-info', 'wpfnl-import-notice' ],
80 'type' => 'update-plugin',
81 'dismissible' => true,
82 'icon' => '',
83 );
84 Admin_Notice::print_notice( $options );
85 }
86 }
87 }
88
89 /**
90 * Schedule the import of funnels as a background process.
91 *
92 * This method is responsible for scheduling the import of funnels as a background process. It checks if the 'wpfnl_import_funnels'
93 * action has not already been scheduled, and if so, it prepares the necessary file for import and schedules the background process.
94 * The process involves uploading the imported file, updating the import notice, and enqueueing the 'wpfnl_import_funnels' action
95 * to be executed asynchronously using a background processing mechanism (such as WP Async Task).
96 *
97 * @since 1.9.7
98 * @updated 2.5.14 - Added security checks
99 */
100 public function schedule_import_funnels() {
101 // Security: Check if user is logged in
102 if ( ! is_user_logged_in() ) {
103 wp_send_json_error( [ 'message' => __( 'Unauthorized access.', 'wpfnl-pro' ) ], 401 );
104 wp_die();
105 }
106
107 // Security: Check user capability
108 if ( ! current_user_can( 'manage_options' ) ) {
109 wp_send_json_error( [ 'message' => __( 'You do not have permission to import funnels.', 'wpfnl-pro' ) ], 403 );
110 wp_die();
111 }
112
113 // Security: Verify nonce
114 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'wpfnl_import_funnels_nonce' ) ) {
115 wp_send_json_error( [ 'message' => __( 'Security check failed.', 'wpfnl-pro' ) ], 403 );
116 wp_die();
117 }
118
119 $is_pro = apply_filters( 'wpfunnels/is_pro_license_activated', false );
120
121 if( !as_has_scheduled_action( 'wpfnl_import_funnels' ) ) {
122 $file = $this->upload_imported_file();
123 if( is_wp_error( $file ) ) {
124 wp_send_json_error( [ 'message' => $file->get_error_message() ], 400 );
125 wp_die();
126 }
127 if( file_exists( $file ) ) {
128 update_option( 'wpfnl_import_notice', 'Funnels import in-progress...' );
129 as_enqueue_async_action( 'wpfnl_import_funnels', [ $file ], 'wpfunnels-import' );
130 wp_send_json_success( [ 'message' => __( 'Funnel import scheduled successfully.', 'wpfnl-pro' ) ] );
131 } else {
132 wp_send_json_error( [ 'message' => __( 'Failed to upload file.', 'wpfnl-pro' ) ], 400 );
133 }
134 } else {
135 wp_send_json_error( [ 'message' => __( 'Import already scheduled.', 'wpfnl-pro' ) ], 400 );
136 }
137 wp_die();
138 }
139
140 /**
141 * Uploads an imported file to the specified directory in the WordPress uploads folder.
142 *
143 * This private method is used to upload an imported file to a specific directory within the WordPress uploads folder.
144 * It ensures that the target directory exists and creates it if not present. The method reads the temporary uploaded
145 * file, extracts its content, and saves it to the target file location. The saved file's name is based on the original
146 * uploaded filename, and if no filename is provided, a default name is used.
147 *
148 * @return string|WP_Error The full path to the uploaded file on success, or WP_Error on failure.
149 * @since 1.9.7
150 * @updated 2.5.14 - Added comprehensive security validation
151 */
152 private function upload_imported_file() {
153 // Security: Validate file upload exists
154 if ( ! isset( $_FILES['uploaded_file'] ) || ! isset( $_FILES['uploaded_file']['tmp_name'] ) ) {
155 return new \WP_Error( 'no_file', __( 'No file uploaded.', 'wpfnl-pro' ) );
156 }
157
158 // Security: Check for upload errors
159 if ( $_FILES['uploaded_file']['error'] !== UPLOAD_ERR_OK ) {
160 return new \WP_Error( 'upload_error', __( 'File upload failed.', 'wpfnl-pro' ) );
161 }
162
163 $temp_file = sanitize_text_field( wp_unslash( $_FILES['uploaded_file']['tmp_name'] ) );
164 $file_name = sanitize_file_name( wp_unslash( $_FILES['uploaded_file']['name'] ) );
165 $file_size = intval( $_FILES['uploaded_file']['size'] );
166
167 // Security: Validate file is not empty
168 if ( empty( $temp_file ) || ! file_exists( $temp_file ) ) {
169 return new \WP_Error( 'invalid_file', __( 'Invalid file upload.', 'wpfnl-pro' ) );
170 }
171
172 // Security: Validate file size (max 10MB)
173 $max_size = 10 * 1024 * 1024; // 10MB
174 if ( $file_size > $max_size || $file_size === 0 ) {
175 return new \WP_Error( 'file_too_large', __( 'File size must be between 1 byte and 10MB.', 'wpfnl-pro' ) );
176 }
177
178 // Security: Validate file extension
179 $file_ext = strtolower( pathinfo( $file_name, PATHINFO_EXTENSION ) );
180 if ( $file_ext !== 'json' ) {
181 return new \WP_Error( 'invalid_extension', __( 'Only JSON files are allowed.', 'wpfnl-pro' ) );
182 }
183
184 // Security: Validate MIME type
185 $file_type = wp_check_filetype( $file_name, [ 'json' => 'application/json' ] );
186 if ( ! $file_type['ext'] ) {
187 return new \WP_Error( 'invalid_mime', __( 'Invalid file type. Only JSON files are allowed.', 'wpfnl-pro' ) );
188 }
189
190 // Security: Read and validate JSON content
191 $content = file_get_contents( $temp_file );
192 if ( $content === false ) {
193 return new \WP_Error( 'read_error', __( 'Failed to read file content.', 'wpfnl-pro' ) );
194 }
195
196 // Security: Validate JSON structure
197 $decoded = json_decode( $content, true );
198 if ( json_last_error() !== JSON_ERROR_NONE ) {
199 return new \WP_Error( 'invalid_json', __( 'Invalid JSON format.', 'wpfnl-pro' ) );
200 }
201
202 // Security: Check for PHP open tags in content.
203 // NOTE: `<script` and `javascript:` are deliberately NOT checked here. Funnel steps
204 // legitimately carry marketing tags (Meta Pixel, gtag) and page-builder link fields
205 // inside their page content, and scanning the raw payload for them rejected valid
206 // exports. That content is stored as post data and is never executed by the importer.
207 if ( preg_match( '/<\?php|<\?=/i', $content ) ) {
208 return new \WP_Error( 'php_code', __( 'File contains PHP code and cannot be imported.', 'wpfnl-pro' ) );
209 }
210
211 // Security: Confirm the payload is actually a funnel export rather than arbitrary JSON.
212 if ( ! $this->is_valid_funnel_export( $decoded ) ) {
213 return new \WP_Error( 'invalid_structure', __( 'This file is not a valid WPFunnels export.', 'wpfnl-pro' ) );
214 }
215
216 $path = wp_upload_dir();
217 $path = $path['basedir'] . '/wpfunnels/import/';
218
219 // Make directory if not exist
220 if ( ! file_exists( $path ) ) {
221 wp_mkdir_p( $path );
222 }
223
224 // Security: Use unique filename to prevent overwrites
225 $unique_filename = wp_unique_filename( $path, $file_name );
226 $file_path = $path . $unique_filename;
227
228 // Security: Write file with restricted permissions
229 $bytes_written = file_put_contents( $file_path, $content );
230 if ( $bytes_written === false ) {
231 return new \WP_Error( 'write_error', __( 'Failed to save file.', 'wpfnl-pro' ) );
232 }
233
234 // Security: Set proper file permissions (read/write for owner only)
235 chmod( $file_path, 0600 );
236
237 return $file_path;
238 }
239
240
241 /**
242 * Determine whether a decoded payload matches the funnel export format.
243 *
244 * A valid export is a non-empty list of funnel objects, each carrying the keys that
245 * `import_funnels()` reads unconditionally. Validating the shape up front rejects
246 * arbitrary JSON before it is written to disk and queued, and surfaces a clear error
247 * to the user instead of failing halfway through the background import.
248 *
249 * @param mixed $decoded The decoded JSON payload.
250 *
251 * @return bool True when the payload is a well-formed funnel export.
252 * @since 2.5.15
253 */
254 private function is_valid_funnel_export( $decoded ) {
255 if ( ! is_array( $decoded ) || empty( $decoded ) ) {
256 return false;
257 }
258
259 foreach ( $decoded as $funnel ) {
260 if ( ! is_array( $funnel ) ) {
261 return false;
262 }
263 if ( empty( $funnel['funnel_id'] ) ) {
264 return false;
265 }
266 if ( ! isset( $funnel['funnel_meta'] ) || ! is_array( $funnel['funnel_meta'] ) ) {
267 return false;
268 }
269 if ( ! isset( $funnel['steps_data'] ) || ! is_array( $funnel['steps_data'] ) ) {
270 return false;
271 }
272 }
273
274 return true;
275 }
276
277
278 /**
279 * Unserialize an imported meta value without instantiating PHP objects.
280 *
281 * Behaves like `maybe_unserialize()` but passes `allowed_classes => false`, so a crafted
282 * `O:` payload in an import file resolves to `__PHP_Incomplete_Class` instead of a real
283 * object. That blocks PHP object injection (POP chains via `__wakeup`/`__destruct`),
284 * which is the one genuinely executable path in the import flow.
285 *
286 * @param mixed $original The value to unserialize.
287 *
288 * @return mixed The unserialized value, or the original value when it is not serialized.
289 * @since 2.5.15
290 */
291 private function safe_maybe_unserialize( $original ) {
292 if ( ! is_string( $original ) ) {
293 return $original;
294 }
295
296 $trimmed = trim( $original );
297 if ( ! is_serialized( $trimmed ) ) {
298 return $original;
299 }
300
301 $result = @unserialize( $trimmed, [ 'allowed_classes' => false ] ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
302
303 // `unserialize()` returns false both on failure and for the serialized value `false`.
304 if ( false === $result && 'b:0;' !== $trimmed ) {
305 return $original;
306 }
307
308 return $result;
309 }
310
311
312 /**
313 * Determine which page builder a step was actually built with.
314 *
315 * Resolved from the step's own exported meta rather than `get_builder_type()`, which reads
316 * the destination site's WPFunnels setting (default `gutenberg`) and says nothing about the
317 * funnel being imported. Relying on that setting meant an Elementor funnel imported into a
318 * site that had not switched its builder skipped Elementor's post-import pass entirely,
319 * leaving images unmapped and the source site's render caches in place.
320 *
321 * Mirrors the detection order already used by `Wpfnl_functions::get_page_builder_by_step_id()`.
322 *
323 * @param array $step_data Exported step payload, expected to carry a `meta` array.
324 *
325 * @return string Builder slug, falling back to the site setting when nothing is detected.
326 * @since 2.5.15
327 */
328 private function detect_step_builder( $step_data ) {
329 $meta = isset( $step_data['meta'] ) && is_array( $step_data['meta'] ) ? $step_data['meta'] : [];
330
331 $value = function( $key ) use ( $meta ) {
332 return isset( $meta[ $key ][0] ) ? $meta[ $key ][0] : '';
333 };
334
335 if ( ! empty( $value( '_elementor_edit_mode' ) ) || ! empty( $value( '_elementor_data' ) ) ) {
336 return 'elementor';
337 }
338 if ( 'on' === $value( '_et_pb_use_builder' ) ) {
339 return 'divi-builder';
340 }
341 if ( 'bricks' === $value( '_bricks_editor_mode' ) ) {
342 return 'bricks';
343 }
344 if ( ! empty( $value( 'ct_builder_shortcodes' ) ) ) {
345 return 'oxygen';
346 }
347
348 return Wpfnl_functions::get_builder_type();
349 }
350
351
352 /**
353 * Run the page-builder specific post-import pass for a freshly created step.
354 *
355 * For Elementor this is what remaps media through `Control_Media::on_import()`, pulling
356 * remote images into this site's library instead of leaving them pointed at the source.
357 *
358 * @param int $step_id Newly created step id.
359 * @param string $builder_type Builder slug from `detect_step_builder()`.
360 *
361 * @return void
362 * @since 2.5.15
363 */
364 private function run_builder_post_import( $step_id, $builder_type ) {
365 // Every source below fetches remote assets while processing the template.
366 if ( ! ini_get( 'allow_url_fopen' ) ) {
367 return;
368 }
369
370 if ( 'gutenberg' === $builder_type && class_exists( '\WPFunnels\Batch\Gutenberg\Wpfnl_Gutenberg_Source' ) ) {
371 $gutenberg_source = new \WPFunnels\Batch\Gutenberg\Wpfnl_Gutenberg_Source;
372 $gutenberg_source->import_single_template( $step_id );
373 }
374
375 if ( 'elementor' === $builder_type && class_exists( '\Elementor\Plugin' ) && class_exists( '\WPFunnels\Batch\Elementor\Wpfnl_Elementor_Source' ) ) {
376 $elementor_source = new \WPFunnels\Batch\Elementor\Wpfnl_Elementor_Source;
377 $elementor_source->import_single_template( $step_id );
378 // Flushed once after the whole import instead of per step; clear_cache() globs and
379 // unlinks the entire Elementor CSS directory on every call.
380 $this->needs_elementor_cache_flush = true;
381 }
382
383 if ( 'divi-builder' === $builder_type && class_exists( '\WPFunnels\Batch\Divi\Wpfnl_Divi_Source' ) ) {
384 $divi_source = new \WPFunnels\Batch\Divi\Wpfnl_Divi_Source;
385 $divi_source->import_single_template( $step_id );
386 }
387
388 if ( 'oxygen' === $builder_type && class_exists( '\WPFunnels\Batch\Oxygen\Wpfnl_Oxygen_Source' ) ) {
389 $oxygen_source = new \WPFunnels\Batch\Oxygen\Wpfnl_Oxygen_Source;
390 $oxygen_source->import_single_template( $step_id );
391 }
392 }
393
394
395 /**
396 * Import Funnel data and create
397 *
398 * @param $file
399 *
400 * @return true
401 * @since 1.7.5
402 */
403 public function import_funnels( $file ) {
404 $file_data = [];
405 if( file_exists( $file ) ) {
406 $file_data = json_decode( file_get_contents( $file ), true );
407 unlink( $file );
408 }
409 $message = __( 'The funnels import process has been successfully completed.', 'wpfnl-pro' );
410
411 if( is_array( $file_data ) ) {
412 $funnel = Wpfnl::$instance->funnel_store;
413 $exclude_meta = array( '_is_imported', '_steps_order', '_steps', '_funnel_data');
414 $is_pro = apply_filters( 'wpfunnels/is_pro_license_activated', false );
415 foreach( $file_data as $data ) {
416 if( empty( $data[ 'funnel_id' ] ) ) {
417 $message = __( 'Invalid funnel(s) data.', 'wpfnl-pro' );
418 break;
419 }
420
421 if( !$is_pro ) {
422 $imported_type = isset( $data['funnel_meta']['_wpfnl_funnel_type'][0] ) ? $data['funnel_meta']['_wpfnl_funnel_type'][0] : '';
423 $is_importing_sc = ( 'store_checkout' === $imported_type );
424 if ( $is_importing_sc ) {
425 $current_count = \WPFunnels\Wpfnl_functions::count_store_checkout_funnels();
426 if ( $current_count >= 3 ) {
427 $message = __( 'You have reached the maximum limit of 3 store checkouts on the free plan. Upgrade to Pro for unlimited store checkouts.', 'wpfnl' );
428 break;
429 }
430 } else {
431 $current_count = \WPFunnels\Wpfnl_functions::count_non_store_checkout_funnels();
432 if( $current_count >= 3 ) {
433 $message = __( 'You have reached the maximum limit of 3 funnels on the free plan. Upgrade to Pro for unlimited funnels.', 'wpfnl' );
434 break;
435 }
436 }
437 }
438
439 $funnel_data = [];
440 $funnel_name = $data[ 'funnel_name' ] ?? 'New Funnel';
441 $funnel_id = $funnel->create( $funnel_name );
442 $funnel->update_meta( $funnel_id, '_is_imported', 'yes' );
443
444 foreach( $data[ 'funnel_meta' ] as $meta_key => $meta ) {
445 if( !isset( $data[ 'funnel_meta' ][ $meta_key ][ 0 ] ) ) {
446 continue;
447 }
448 if( in_array( $meta_key, Wpfnl_functions::VOLATILE_TRANSFER_META_KEYS, true ) ) {
449 continue;
450 }
451 if( !in_array( $meta_key, $exclude_meta ) ) {
452 $funnel->update_meta( $funnel_id, $meta_key, $this->safe_maybe_unserialize( $data[ 'funnel_meta' ][ $meta_key ][ 0 ] ) );
453 }
454 else {
455 if( 'funnel_data' === $meta_key ) {
456 $funnel_data = $this->safe_maybe_unserialize( $data[ 'funnel_meta' ][ $meta_key ][ 0 ] );
457 }
458
459 if( '_funnel_data' === $meta_key ) {
460 $funnel_data = $this->safe_maybe_unserialize( $data[ 'funnel_meta' ][ $meta_key ][ 0 ] );
461 }
462 }
463 }
464 $_new_step_ids = [];
465 $_new_step_order = [];
466
467 foreach( $data[ 'steps_data' ] as $step_data ) {
468 $response = $this->import_step( $funnel_id, $step_data );
469 if( isset( $response[ 'id' ], $response[ 'title' ], $response[ 'type' ] ) ) {
470 $_new_step_ids[ $step_data[ 'id' ] ] = $response[ 'id' ];
471 $_new_step_order[] = [
472 'id' => $response[ 'id' ],
473 'step_type' => $response[ 'type' ],
474 'name' => $response[ 'title' ],
475 ];
476 }
477 else {
478 $message = __( 'Invalid funnel(s) data.', 'wpfnl-pro' );
479 break;
480 }
481 }
482
483 $this->update_funnel_data( $_new_step_ids, $funnel_id, $funnel_data );
484 update_post_meta( $funnel_id, '_steps_order', $_new_step_order );
485 update_post_meta( $funnel_id, '_steps', $_new_step_order );
486
487 $instance = new Wpfnl_functions();
488 if( method_exists( $instance, 'generate_first_step' ) ) {
489 Wpfnl_functions::generate_first_step( $funnel_id, [] );
490 }
491 }
492 }
493
494 // Flush Elementor's file cache once for the whole import. clear_cache() unlinks the
495 // entire Elementor CSS directory and runs three delete_post_meta_by_key() sweeps, so
496 // doing it per step multiplied that by the step count.
497 if( $this->needs_elementor_cache_flush && class_exists( '\Elementor\Plugin' ) ) {
498 Plugin::$instance->files_manager->clear_cache();
499 $this->needs_elementor_cache_flush = false;
500 }
501
502 update_option( 'wpfnl_import_notice', $message );
503 return true;
504 }
505
506
507 /**
508 * Import step from json
509 * @param Int $funnel_id
510 * @param Array $step_data
511 * @return
512 * @since 1.7.5
513 *
514 */
515 public function import_step($funnel_id, $step_data){
516 if (!is_array($step_data)) {
517 return false;
518 }
519 $builder_type = $this->detect_step_builder($step_data);
520 $previous_step_id = isset($step_data['id']) ? $step_data['id'] : '';
521 $title = isset($step_data['title']) ? $step_data['title'] : '';
522 $slug = isset($step_data['slug']) ? $step_data['slug'] : '';
523 $page_template = isset($step_data['page_template']) ? $step_data['page_template'] : '';
524 $post_content = isset($step_data['post_content']) ? $step_data['post_content'] : '';
525 $step_type = isset($step_data['type']) ? $step_data['type'] : '';
526 $step = new Wpfnl_Steps_Store_Data();
527 $step_id = $step->create_step($funnel_id, $title, $step_type, $post_content, true);
528
529 if ($step_id) {
530
531 if ($slug) {
532 wp_update_post(array(
533 'ID' => $step_id,
534 'post_name' => $slug
535 ));
536 }
537
538 $step->update_meta($step_id, '_funnel_id', $funnel_id);
539 $step->update_meta($step_id, '_wp_page_template', $page_template);
540 $exclude_meta = array_merge(
541 array('_wpfnl_ab_testing_start_settings', '_funnel_id'),
542 Wpfnl_functions::VOLATILE_TRANSFER_META_KEYS
543 );
544 if ( ! Wpfnl_functions::is_wpfnl_pro_activated() ) {
545 $exclude_meta = array_merge( $exclude_meta, array(
546 '_wpfnl_maybe_enable_condition',
547 '_wpfnl_step_conditions',
548 '_wpfnl_next_step_after_condition',
549 ) );
550 }
551 $sql_query_arr = [];
552 $is_enable_condition_meta_blank = false;
553 $is_condition_exits = false;
554 foreach ($step_data['meta'] as $meta_key => $meta) {
555
556 if (isset($meta[0])) {
557 if (!in_array($meta_key, $exclude_meta)) {
558 if ('_wpfnl_maybe_enable_condition' === $meta_key) {
559 if ('' === $meta[0]) {
560 $is_enable_condition_meta_blank = true;
561 }
562 }
563
564 if ('_wpfnl_step_conditions' === $meta_key) {
565 if ($meta[0]) {
566 $is_condition_exits = true;
567 }
568 }
569
570 $sql_data = [
571 'post_id' => $step_id,
572 'meta_key' => $meta_key,
573 'meta_value' => $meta[0],
574 ];
575 $sql_query_arr[] = $sql_data;
576 } elseif ('_wpfnl_ab_testing_start_settings' === $meta_key) {
577
578 $this->update_ab_testing_settings($funnel_id, $step_id, $meta[0], $step_data['ab_step_data']);
579 }
580 }
581 }
582 $this->update_meta_by_sql($sql_query_arr); // update meta
583
584 if ($is_enable_condition_meta_blank && $is_condition_exits) {
585 update_post_meta($step_id, '_wpfnl_maybe_enable_condition', 'yes');
586 }
587
588 $this->run_builder_post_import($step_id, $builder_type);
589
590 delete_post_meta($step_id, '_funnel_id');
591 update_post_meta($step_id, '_funnel_id', $funnel_id);
592
593 return [
594 'id' => $step_id,
595 'type' => $step_type,
596 'title' => $title,
597 ];
598 }
599
600 return false;
601 }
602
603
604 /**
605 * Update post meta by raw SQL.
606 *
607 * Inserts post meta data into the WordPress database using raw SQL queries.
608 *
609 * @param array $sql_query_arr An array of SQL query values in the format (post_id, meta_key, meta_value).
610 *
611 * @since 1.8.10
612 * @return boolean
613 */
614 public function update_meta_by_sql( $sql_query_arr ){
615 if ( ! is_array( $sql_query_arr ) || empty( $sql_query_arr ) ) {
616 return false;
617 }
618 global $wpdb;
619 $insert_sql_query = "INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value) VALUES ";
620 $value_placeholders = array();
621 $query_args = array();
622 foreach( $sql_query_arr as $query ) {
623 if( isset($query['post_id'],$query['meta_key'],$query['meta_value']) ){
624 $value_placeholders[] = '(%d, %s, %s)';
625 $query_args[] = $query['post_id'];
626 $query_args[] = $query['meta_key'];
627 $query_args[] = $query['meta_value'];
628 }
629 }
630 if( empty( $value_placeholders ) || empty( $query_args )){
631 return false;
632 }
633 $insert_sql_query = rtrim( $insert_sql_query, ',' );
634 $insert_sql_query .= ' ' . implode( ',', $value_placeholders );
635 $prepared_query = $wpdb->prepare( $insert_sql_query, $query_args );
636 $result = $wpdb->query( $prepared_query );
637 return false !== $result;
638 }
639
640
641 /**
642 * Update A/B testing settings for a funnel step.
643 * This function updates the A/B testing settings for a funnel step with the provided parameters.
644
645 * @param int $funnel_id The ID of the funnel to which the step belongs.
646 * @param int $step_id The ID of the step for which the A/B testing settings are being updated.
647 * @param mixed $settings The new A/B testing settings for the step. This can be a serialized string or an array.
648 * @param mixed $prev_settings The previous A/B testing settings for the step. This can be a serialized string or an array.
649 *
650 * @since 1.8.8
651 * @return void
652 */
653 public function update_ab_testing_settings( $funnel_id, $step_id, $settings, $prev_settings ){
654 $settings = $this->safe_maybe_unserialize($settings);
655 $prev_settings = $this->safe_maybe_unserialize($prev_settings);
656
657 if( isset( $settings['variations'] ) ){
658 $new_settings = [];
659 $new_settings['isStart'] = 'no';
660 $new_settings['startDate'] = isset($settings['startDate']) ?$settings['startDate'] : date( 'Y-m-d H:i:s' );
661 $new_settings['endDate'] = isset($settings['endDate']) ?$settings['endDate'] : date( 'Y-m-d H:i:s' );
662 $new_settings['variations'] = $this->update_ab_variations( $funnel_id, $step_id, $settings['variations'], $prev_settings );
663 update_post_meta( $step_id, '_wpfnl_ab_testing_start_settings' , $new_settings );
664 }
665
666 }
667
668
669 /**
670 * Update A/B testing variations for a funnel step.
671 * This function updates the A/B testing variations for a funnel step with the provided parameters.
672 *
673 * @param int $funnel_id The ID of the funnel to which the step belongs.
674 * @param int $step_id The ID of the step for which the A/B testing variations are being updated.
675 * @param array $variations The new A/B testing variations for the step.
676 * @param mixed $prev_settings The previous A/B testing settings for the step.
677 *
678 * @return array The updated A/B testing variations.
679 * @since 1.8.8
680 */
681 public function update_ab_variations( $funnel_id, $step_id, $variations, $prev_settings ){
682 if( count( $variations ) >= 2 ){
683 $step = new Wpfnl_Steps_Store_Data();
684 foreach( $variations as $variation_key=>$variation ){
685
686 if( !isset($variation['stepId'], $variation['variationType'] ) ){
687 continue;
688 }
689
690 $key = array_search($variation['stepId'], array_column($prev_settings, 'step_id'));
691 if( false !== $key ){
692
693 if ( 'original' !== $variation['variationType'] && isset($prev_settings[$key]['meta']) ){
694 $metas = $prev_settings[$key]['meta'];
695 $exclude_meta = array_merge(
696 array( '_wpfnl_ab_testing_start_settings','_wpfnl_automation_steps', '_parent_step_id' ),
697 Wpfnl_functions::VOLATILE_TRANSFER_META_KEYS
698 );
699 $page_template = isset( $metas['page_template'] ) ? $metas['page_template'] : '';
700 $title = isset( $metas['title'] ) ? $metas['title'] : '';
701 $post_content = isset( $metas['post_content'] ) ? $metas['post_content'] : '';
702 $step_type = isset( $metas['type'] ) ? $metas['type'] : '';
703 if( $step ){
704 $new_step_id = $step->create_step($funnel_id, $title, $step_type, $post_content, true);
705 if( $new_step_id ){
706 $step->update_meta($new_step_id, '_funnel_id', $funnel_id);
707 $step->update_meta($new_step_id, '_wp_page_template', $page_template);
708 $sql_query_arr = [];
709
710 foreach( $metas['meta'] as $meta_key=>$meta ){
711 if( !in_array( $meta_key, $exclude_meta ) ){
712 $sql_data = [
713 'post_id' => $new_step_id,
714 'meta_key' => $meta_key,
715 'meta_value'=> $meta[0],
716 ];
717 $sql_query_arr[] = $sql_data;
718 }
719 }
720 $this->update_meta_by_sql( $sql_query_arr ); // update meta
721 // Variants carry their own builder content, so they need the
722 // same post-import pass as a regular step.
723 $this->run_builder_post_import( $new_step_id, $this->detect_step_builder( $metas ) );
724 }
725 }
726
727 }else{
728 $new_step_id = $step_id;
729 }
730 update_post_meta( $new_step_id, '_parent_step_id', $step_id );
731 $variations[$variation_key]['id'] = $new_step_id;
732 $variations[$variation_key]['stepType'] = get_post_meta( $step_id, '_step_type', true );
733 $step_edit_link = get_edit_post_link($new_step_id);
734 // `ab_step_data[]` nests the full step payload under its `meta` key
735 // (see Wpfnl_Export::export_ab_step_metas()), so detection reads that.
736 $variation_step_data = isset( $prev_settings[$key]['meta'] ) ? $prev_settings[$key]['meta'] : [];
737 if( 'elementor' === $this->detect_step_builder( $variation_step_data ) ){
738 $step_edit_link = str_replace('&amp;','&',$step_edit_link);
739 $step_edit_link = str_replace('edit','elementor',$step_edit_link);
740 }
741 $variations[$variation_key]['stepEditLink'] = $step_edit_link;
742 $variations[$variation_key]['stepViewLink'] = get_the_permalink($new_step_id);
743 $variations[$variation_key]['stepName'] = get_the_title($new_step_id);
744 $variations[$variation_key]['stepId'] = $new_step_id;
745 }
746 }
747 }else{
748 $default_data = Wpfnl_Ab_Testing::get_default_data( $step_id );
749 $variations = $default_data['variations'];
750 }
751 return $variations;
752 }
753
754
755
756 /**
757 * Update funnel data and steps
758 *
759 * @param Int $funnel_id
760 * @param Array $step_data
761 *
762 * @return void
763 * @since 1.7.5
764 */
765 public function update_funnel_data( $new_step_ids, $funnel_id, $funnel_data ){
766 $funnel_identifier = array();
767 if ($funnel_data) {
768 $node_data = isset($funnel_data['drawflow']['Home']['data']) ? $funnel_data['drawflow']['Home']['data'] : [];
769 foreach ($node_data as $node_key => $node_value) {
770 if(isset($node_value['data']['step_id'])) {
771 if ( isset($new_step_ids[$node_value['data']['step_id']]) ) {
772 $post_edit_link = base64_encode(get_edit_post_link($new_step_ids[$node_value['data']['step_id']]));
773 $post_view_link = base64_encode(get_post_permalink($new_step_ids[$node_value['data']['step_id']]));
774 $funnel_data['drawflow']['Home']['data'][$node_key]['data']['step_id'] = $new_step_ids[$node_value['data']['step_id']];
775 $funnel_data['drawflow']['Home']['data'][$node_key]['data']['step_edit_link'] = $post_edit_link;
776 $funnel_data['drawflow']['Home']['data'][$node_key]['data']['step_view_link'] = $post_view_link;
777 $funnel_data['drawflow']['Home']['data'][$node_key]['html'] = $node_value['data']['step_type'] . $new_step_ids[$node_value['data']['step_id']];
778 $funnel_identifier[$node_value['id']] = $new_step_ids[$node_value['data']['step_id']];
779 } else {
780 if ($node_value['data']['step_type'] != 'conditional') {
781 $funnel_identifier[$node_value['id']] = $node_value['data']['step_id'];
782 } else {
783 $funnel_identifier[$node_value['id']] = $node_value['data']['node_identifier'];
784 }
785 }
786 }
787 if ($node_value['data']['step_type'] == 'conditional') {
788 $funnel_identifier[$node_value['id']] = $node_value['data']['node_identifier'];
789 }
790 }
791 }
792 update_post_meta($funnel_id, '_funnel_data', $funnel_data);
793 if ($funnel_identifier) {
794 $funnel_identifier_json = json_encode($funnel_identifier, JSON_UNESCAPED_SLASHES);
795 update_post_meta($funnel_id, 'funnel_identifier', $funnel_identifier_json);
796 }
797 }
798
799 /**
800 * Hide the import funnel notice based on nonce verification.
801 *
802 * This method is responsible for hiding the import funnel notice displayed to users. It receives a nonce value from the
803 * submitted form, verifies the nonce using the 'wp_verify_nonce' function, and if the verification succeeds, it deletes the
804 * 'wpfnl_import_notice' option to hide the notice. It then sends a JSON success response to the client and terminates the script
805 * execution using 'wp_die'.
806 *
807 * @since 1.9.7
808 */
809 public function hide_import_funnel_notice() {
810 $nonce = !empty( $_POST[ 'security' ] ) ? htmlspecialchars( trim( $_POST[ 'security' ] ) ) : null; // phpcs:ignore
811
812 if( wp_verify_nonce( $nonce, 'wpfnl-admin' ) ) {
813 delete_option( 'wpfnl_import_notice' );
814 }
815 wp_send_json_success();
816 wp_die();
817 }
818
819 public function get_validation_data()
820 {
821 return [
822 'logged_in' => true,
823 'user_can' => 'wpf_manage_funnels',
824 ];
825 }
826 }