PluginProbe
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster / 2.7.0
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster v2.7.0
2.7.0 2.6.0 trunk 1.1.0 1.1.4 1.2.1 1.2.2 1.2.3 1.2.5 1.2.9 1.3.1 1.3.2 1.5.0 1.5.1 1.5.4 1.5.7 1.5.8 1.5.9 1.6.2 1.6.5 1.6.8 1.7.2 1.7.4 1.7.5 1.7.9 All 43 releases
← All changes | includes/admin/components/class-list-table.php +375 -0 1.3.22.7.0 View file →
@@ -1,13 +1,18 @@
1 1 <?php
2 2
3 3 defined( 'ABSPATH' ) || die();
4 4
5 +use Elementor\Plugin;
6 +use Sellkit\Admin\Funnel\Importer\Ajax_Handler;
7 +use Sellkit\Global_Checkout\Checkout;
8 +
5 9 /**
6 10 * List Table component class.
7 11 *
8 12 * @since 1.1.0
9 13 * @SuppressWarnings(PHPMD.UnusedPrivateMethod)
14 + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
10 15 */
11 16 class Sellkit_Admin_List_Table {
12 17
13 18 /**
@@ -55,8 +60,13 @@
55 60 $post_type = sellkit_htmlspecialchars( INPUT_POST, 'post_type' );
56 61 $paged = filter_input( INPUT_POST, 'paged', FILTER_SANITIZE_NUMBER_INT );
57 62 $posts_per_page = filter_input( INPUT_POST, 'posts_per_page', FILTER_SANITIZE_NUMBER_INT );
58 63 $search_args = filter_input( INPUT_POST, 'args', FILTER_DEFAULT, FILTER_FORCE_ARRAY );
64 + $page = filter_input( INPUT_POST, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
65 + $list_status = isset( $_POST['list_status'] ) ? sanitize_text_field( wp_unslash( $_POST['list_status'] ) ) : 'all';
66 + if ( ! in_array( $list_status, [ 'all', 'publish', 'draft' ], true ) ) {
67 + $list_status = 'all';
68 + }
59 69
60 70 /**
61 71 * Filter List Table query arguments.
62 72 *
@@ -77,8 +87,22 @@
77 87 $args[ $search_arg['param'] ] = $search_arg['value'];
78 88 }
79 89 }
80 90
91 + $not_in = $this->get_list_table_post__not_in( $post_type, $page );
92 + if ( ! empty( $not_in ) ) {
93 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Exclude global checkout funnel from list.
94 + $args['post__not_in'] = $not_in;
95 + }
96 +
97 + if ( 'publish' === $list_status ) {
98 + $args['post_status'] = 'publish';
99 + } elseif ( 'draft' === $list_status ) {
100 + $args['post_status'] = 'draft';
101 + } else {
102 + $args['post_status'] = [ 'publish', 'draft', 'pending', 'private', 'future' ];
103 + }
104 +
81 105 // Query.
82 106 $query = new \WP_Query( $args );
83 107
84 108 /**
@@ -88,8 +112,9 @@
88 112 *
89 113 * @param array $args The taxonomy arguments.
90 114 */
91 115 $posts = apply_filters( "sellkit_list_table_{$post_type}_posts", $query->posts );
116 + $posts = $this->prepare_posts_for_list_table( $posts );
92 117
93 118 /**
94 119 * Filter List Table columns.
95 120 *
@@ -101,17 +126,109 @@
101 126 'labels' => [],
102 127 'values' => [],
103 128 ], $posts );
104 129
130 + $list_counts = $this->get_list_status_counts( $post_type, $page );
131 +
105 132 // Send response.
106 133 wp_send_json_success( [
107 134 'posts' => $posts,
108 135 'max_num_pages' => $query->max_num_pages,
109 136 'columns' => $columns,
137 + 'list_counts' => $list_counts,
110 138 ] );
111 139 }
112 140
113 141 /**
142 + * Post IDs excluded from list queries (e.g. global checkout funnel).
143 + *
144 + * @since 2.4.0
145 + *
146 + * @param string $post_type Post type.
147 + * @param string $page Admin page slug context.
148 + * @return int[]
149 + */
150 + private function get_list_table_post__not_in( $post_type, $page ) {
151 + if ( 'sellkit-funnels' === $post_type && 'checkout' !== $page ) {
152 + $global_checkout_id = (int) get_option( Checkout::SELLKIT_GLOBAL_CHECKOUT_OPTION, 0 );
153 + if ( $global_checkout_id > 0 ) {
154 + return [ $global_checkout_id ];
155 + }
156 + }
157 + return [];
158 + }
159 +
160 + /**
161 + * Counts for All / Active / Inactive list filters (respects post__not_in).
162 + *
163 + * @since 2.4.0
164 + *
165 + * @param string $post_type Post type.
166 + * @param string $page Admin page slug context.
167 + * @return array{all:int,publish:int,draft:int}
168 + */
169 + private function get_list_status_counts( $post_type, $page ) {
170 + $not_in = $this->get_list_table_post__not_in( $post_type, $page );
171 + $base = [
172 + 'post_type' => $post_type,
173 + 'posts_per_page' => -1,
174 + 'fields' => 'ids',
175 + 'no_found_rows' => false,
176 + ];
177 + if ( ! empty( $not_in ) ) {
178 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
179 + $base['post__not_in'] = $not_in;
180 + }
181 +
182 + $statuses_all = [ 'publish', 'draft', 'pending', 'private', 'future' ];
183 + $q_all = new \WP_Query( array_merge( $base, [ 'post_status' => $statuses_all ] ) );
184 + $q_pub = new \WP_Query( array_merge( $base, [ 'post_status' => 'publish' ] ) );
185 + $q_dra = new \WP_Query( array_merge( $base, [ 'post_status' => 'draft' ] ) );
186 +
187 + return [
188 + 'all' => (int) $q_all->found_posts,
189 + 'publish' => (int) $q_pub->found_posts,
190 + 'draft' => (int) $q_dra->found_posts,
191 + ];
192 + }
193 +
194 + /**
195 + * Add formatted published / modified dates for list table rows (JSON).
196 + *
197 + * @since 2.4.0
198 + *
199 + * @param array $posts Posts from WP_Query.
200 + * @return array
201 + */
202 + private function prepare_posts_for_list_table( $posts ) {
203 + if ( empty( $posts ) || ! is_array( $posts ) ) {
204 + return $posts;
205 + }
206 +
207 + $date_time_format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );
208 +
209 + foreach ( $posts as $key => $post ) {
210 + if ( ! $post instanceof \WP_Post ) {
211 + continue;
212 + }
213 +
214 + $row = array_merge(
215 + $post->to_array(),
216 + [
217 + 'custom_date' => get_the_date( 'M d, Y', $post ),
218 + 'custom_date_with_time' => mysql2date( $date_time_format, $post->post_date ),
219 + 'custom_modified_date' => get_the_modified_date( 'M d, Y', $post ),
220 + 'custom_modified_date_with_time' => mysql2date( $date_time_format, $post->post_modified ),
221 + ]
222 + );
223 +
224 + $posts[ $key ] = (object) $row;
225 + }
226 +
227 + return $posts;
228 + }
229 +
230 + /**
114 231 * Handle post actions.
115 232 *
116 233 * @since 1.1.0
117 234 */
@@ -121,8 +238,12 @@
121 238 // Sanitize.
122 239 $post = filter_input( INPUT_POST, 'post', FILTER_DEFAULT, FILTER_FORCE_ARRAY );
123 240 $sub_action = sellkit_htmlspecialchars( INPUT_POST, 'sub_action' );
124 241
242 + if ( is_array( $post ) && count( $post ) < 3 ) {
243 + $post = (array) get_post( $post['ID'] );
244 + }
245 +
125 246 call_user_func( [ $this, "handle_post_{$sub_action}" ], $post );
126 247 }
127 248
128 249 /**
@@ -189,8 +310,9 @@
189 310 * @since 1.1.0
190 311 * @param array $post Post array.
191 312 */
192 313 private function handle_post_remove( $post ) {
314 + do_action( 'sellkit_funnels_after_delete_posts_' . $post['post_type'], $post );
193 315 // Delete post.
194 316 $result = wp_delete_post( $post['ID'] );
195 317
196 318 // Handle error.
@@ -199,8 +321,261 @@
199 321 }
200 322
201 323 // Send success response.
202 324 wp_send_json_success( $result );
325 + }
326 +
327 + /**
328 + * Duplicate funnel item.
329 + *
330 + * @since 1.5.0
331 + * @param array $post Post Array.
332 + */
333 + private function handle_post_duplicate_single_funnel( $post ) {
334 + $post_id = $post['ID'];
335 + $parent_title = sprintf( '%1s - %2s', esc_html( $post['post_title'] ), esc_html__( 'Clone', 'sellkit' ) );
336 + $funnel_data = get_post_meta( $post_id, 'nodes', true );
337 + $nodes = true;
338 +
339 + if ( empty( $funnel_data ) ) {
340 + $funnel_data = get_post_meta( $post_id, 'sellkit_steps', true );
341 + $nodes = false;
342 + }
343 +
344 + $funnel_slug = get_post_meta( $post_id, 'sellkit_post_slug', true );
345 +
346 + // Let's create parent funnel post first.
347 + $new_funnel_id = wp_insert_post( [
348 + 'post_title' => $parent_title,
349 + 'post_status' => 'draft',
350 + 'post_type' => 'sellkit-funnels',
351 + 'meta_input' => [
352 + 'sellkit_post_slug' => $funnel_slug,
353 + ],
354 + ] );
355 +
356 + // Loop through old parent data and create steps posts of new funnel.
357 + foreach ( $funnel_data as $key => $funnel_item ) {
358 + $old_id = $funnel_item['page_id'];
359 +
360 + // Create steps.
361 + $title = $funnel_item['title'];
362 + $args = [
363 + 'post_type' => 'sellkit_step',
364 + 'post_title' => esc_html( $title ),
365 + 'post_status' => get_post_status( $old_id ),
366 + ];
367 +
368 + $args = $this->handle_post_content( $args, $old_id );
369 +
370 + $new_id = wp_insert_post( $args );
371 +
372 + // Update new parent funnel data with new steps id.
373 + $funnel_data[ $key ]['page_id'] = $new_id;
374 + $funnel_data[ $key ]['funnel_id'] = $new_funnel_id;
375 +
376 + // Add old steps meta data to new steps.
377 + $old_data = get_post_meta( $old_id );
378 +
379 + foreach ( $old_data as $key => $data ) {
380 + if ( 'step_data' === $key ) {
381 + $data[0] = str_replace( $post_id, $new_funnel_id, $data[0] );
382 + }
383 +
384 + if ( '_elementor_data' === $key && '[' === substr( $data[0], 0, 1 ) ) {
385 + update_post_meta( $new_id, $key, maybe_unserialize( wp_slash( $data[0] ) ) );
386 +
387 + continue;
388 + }
389 +
390 + update_post_meta( $new_id, $key, maybe_unserialize( $data[0] ) );
391 + }
392 +
393 + if ( class_exists( 'Elementor\Plugin' ) ) {
394 + // Also save post in elementor way. after all it's elementor post.
395 + $document = Plugin::$instance->documents->get( $new_id );
396 + $document->save( [] );
397 + }
398 + }
399 +
400 + // At the end update parent main meta.
401 + if ( false === $nodes ) {
402 + update_post_meta( $new_funnel_id, 'sellkit_steps', $funnel_data );
403 + } else {
404 + update_post_meta( $new_funnel_id, 'nodes', $funnel_data );
405 + }
406 +
407 + wp_send_json_success();
408 + }
409 +
410 + /**
411 + * Handle post content for templates without elementor.
412 + *
413 + * @param array $args funnel nodes arguments.
414 + * @param integer $old_id node id.
415 + * @since 1.5.0
416 + * @return array
417 + */
418 + private function handle_post_content( $args, $old_id ) {
419 + if ( empty( $old_id ) ) {
420 + return $args;
421 + }
422 +
423 + $is_elementor = get_post_meta( $old_id, '_elementor_data', true );
424 +
425 + if ( ! empty( $is_elementor ) ) {
426 + return $args;
427 + }
428 +
429 + $post_content = get_post( $old_id );
430 + $content = $post_content->post_content;
431 +
432 + return array_merge( $args, [ 'post_content' => $content ] );
433 + }
434 +
435 + /**
436 + * Export single funnel.
437 + *
438 + * @since 1.5.0
439 + * @param array $post Post Array.
440 + */
441 + private function export_single_funnel( $post ) {
442 + $post_id = $post['ID'];
443 + $data['funnel']['id'] = $post_id;
444 + $data['funnel']['name'] = $post['post_name'];
445 + $data['funnel']['title'] = $post['post_title'];
446 + $data['funnel']['post_type'] = $post['post_type'];
447 + $data['funnel']['post_status'] = $post['post_status'];
448 + $data['funnel']['sellkit_steps'] = get_post_meta( $post_id, 'nodes', true );
449 +
450 + // Integration with flowchart.
451 + if ( empty( $data['funnel']['sellkit_steps'] ) ) {
452 + $data['funnel']['sellkit_steps'] = get_post_meta( $post_id, 'sellkit_steps', true );
453 + }
454 +
455 + // Check for steps.
456 + if ( ! is_array( $data['funnel']['sellkit_steps'] ) ) {
457 + return $data;
458 + }
459 +
460 + foreach ( $data['funnel']['sellkit_steps'] as $step ) {
461 + if ( ! array_key_exists( 'page_id', $step ) ) {
462 + continue;
463 + }
464 +
465 + $id = $step['page_id'];
466 + $post = get_post( $id );
467 + $export_data = get_post_meta( $id );
468 +
469 + $data['step'][ $id ]['ID'] = $post->ID;
470 + $data['step'][ $id ]['name'] = $post->post_name;
471 + $data['step'][ $id ]['title'] = $post->post_title;
472 + $data['step'][ $id ]['post_type'] = $post->post_type;
473 + $data['step'][ $id ]['post_status'] = $post->post_status;
474 + $data['step'][ $id ]['post_content'] = $post->post_content;
475 + $data['step'][ $id ]['page_id'] = $id;
476 +
477 + foreach ( $export_data as $key => $meta ) {
478 + if ( '_elementor_data' === $key && defined( 'ELEMENTOR_VERSION' ) ) {
479 + $document = Plugin::$instance->documents->get( $id );
480 + $template_data = $document->get_export_data();
481 +
482 + $data['step'][ $id ]['meta'][ $key ] = wp_json_encode( $template_data['content'] );
483 + continue;
484 + }
485 +
486 + $data['step'][ $id ]['meta'][ $key ] = $meta[0];
487 + }
488 + }
489 +
490 + return $data;
491 + }
492 +
493 + /**
494 + * Handle exporting single funnel by ajax.
495 + *
496 + * @since 1.5.0
497 + * @param array $post Post Array.
498 + */
499 + private function handle_post_export_single_funnel( $post ) {
500 + $data = [];
501 + $data['type'] = 'single';
502 + $data['data'] = $this->export_single_funnel( $post );
503 +
504 + wp_send_json_success( $data );
505 + }
506 +
507 + /**
508 + * Handle exporting single funnel by ajax.
509 + *
510 + * @since 1.5.0
511 + */
512 + private function handle_post_export_all_funnel() {
513 + $data = [];
514 + $data['type'] = 'multi';
515 +
516 + $args = [
517 + 'post_type' => 'sellkit-funnels',
518 + 'posts_per_page' => -1,
519 + ];
520 +
521 + $posts = new \WP_Query( $args );
522 + $posts = $posts->posts;
523 +
524 + if ( empty( $posts ) ) {
525 + wp_send_json_error();
526 + }
527 +
528 + $global_checkout_id = intval( get_option( Checkout::SELLKIT_GLOBAL_CHECKOUT_OPTION, 0 ) );
529 +
530 + foreach ( $posts as $post ) {
531 + if ( $post->ID === $global_checkout_id ) {
532 + continue;
533 + }
534 +
535 + $post = (array) $post;
536 +
537 + $data['data'][] = $this->export_single_funnel( $post );
538 + }
539 +
540 + wp_send_json_success( $data );
541 + }
542 +
543 + /**
544 + * Handle import funnel.
545 + *
546 + * @since 1.5.0
547 + */
548 + private function handle_post_import_funnel() {
549 + $json = filter_input( INPUT_POST, 'funnel', FILTER_DEFAULT );
550 + $page = filter_input( INPUT_POST, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
551 + $data = json_decode( $json );
552 +
553 + if ( 'multi' === $data->type && 'checkout' === $page ) {
554 + wp_send_json_error( esc_html__( 'You can not import multiple funnels to the global checkout.', 'sellkit' ) );
555 + }
556 +
557 + $ajax_handler = new Ajax_Handler();
558 +
559 + if ( 'single' === $data->type ) {
560 + $funnel_data = $data->data->funnel;
561 + $steps_data = $data->data->step;
562 + $funnel_id = $ajax_handler->import_funnel_by_data( $funnel_data, $steps_data );
563 + }
564 +
565 + if ( 'multi' === $data->type ) {
566 + foreach ( $data->data as $item ) {
567 + $funnel_data = $item->funnel;
568 + $steps_data = $item->step;
569 + $funnel_id = $ajax_handler->import_funnel_by_data( $funnel_data, $steps_data );
570 + }
571 + }
572 +
573 + if ( 'checkout' === $page && (int) $funnel_id > 0 ) {
574 + update_option( Checkout::SELLKIT_GLOBAL_CHECKOUT_OPTION, $funnel_id );
575 + }
576 +
577 + wp_send_json_success();
203 578 }
204 579 }
205 580
206 581 Sellkit_Admin_List_Table::get_instance();