PluginProbe
Ultimate Store Kit – Store Builder Addons for Elementor, WooCommerce Store Builder, EDD Store Builder / 3.1.4
Ultimate Store Kit – Store Builder Addons for Elementor, WooCommerce Store Builder, EDD Store Builder v3.1.4
3.1.4 3.0.8 3.0.9 3.1.0 3.1.2 3.1.3 3.0.7 3.0.5 3.0.4 3.0.3 3.0.2 trunk 1.5.0 1.5.1 1.5.2 1.6.1 1.6.2 1.6.3 1.6.4 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 All 93 releases
← All changes | includes/builder/builder-cpt.php +201 -97 1.6.33.1.4 View file →
@@ -3,11 +3,11 @@
3 3 namespace UltimateStoreKit\Includes\Builder;
4 4
5 5 use UltimateStoreKit\Base\Singleton;
6 6
7 -if (!defined('ABSPATH')) {
7 +if (! defined('ABSPATH')) {
8 8 exit;
9 -} // Exit if accessed directly
9 +}
10 10
11 11 class Builder_Cpt {
12 12
13 13 use Singleton;
@@ -25,8 +25,13 @@
25 25 add_filter("manage_{$builderCpt}_posts_columns", [$this, 'set_post_columns']);
26 26 add_action("manage_{$builderCpt}_posts_custom_column", [$this, 'set_custom_column_value'], 10, 2);
27 27 add_filter('post_row_actions', [$this, 'post_row_actions_filter'], 20, 2);
28 28
29 + // Simple WPML fix
30 + if (function_exists('icl_object_id')) {
31 + add_filter('elementor/editor/before_enqueue_scripts', [$this, 'fix_wpml_elementor_data'], 1);
32 + }
33 +
29 34 if (is_admin()) {
30 35 add_action('admin_enqueue_scripts', [$this, 'enqueue_scripts'], 1);
31 36 add_action('admin_menu', [$this, 'add_admin_menu'], 202);
32 37 add_action('restrict_manage_posts', [$this, 'add_filter']);
@@ -40,28 +45,32 @@
40 45 global $wpdb;
41 46
42 47 $postType = Meta::POST_TYPE;
43 48
44 - $query = $wpdb->get_results("SELECT {$wpdb->posts}.ID,{$wpdb->posts}.post_type, {$wpdb->posts}.post_status, {$wpdb->postmeta}.meta_value as template_type
45 -FROM $wpdb->posts
46 - LEFT JOIN $wpdb->postmeta
47 - ON {$wpdb->postmeta}.post_id = {$wpdb->posts}.ID
48 -WHERE 1=1
49 -AND {$wpdb->posts}.post_type ='{$postType}'
50 -AND {$wpdb->postmeta}.meta_key ='_ultimate_store_kit_template_type'
51 -ORDER BY {$wpdb->posts}.post_date DESC");
49 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Rebuilds the template cache; this is the call that populates it.
50 + $query = $wpdb->get_results($wpdb->prepare(
51 + "SELECT {$wpdb->posts}.ID, {$wpdb->posts}.post_type, {$wpdb->posts}.post_status, {$wpdb->postmeta}.meta_value as template_type
52 + FROM {$wpdb->posts}
53 + LEFT JOIN {$wpdb->postmeta}
54 + ON {$wpdb->postmeta}.post_id = {$wpdb->posts}.ID
55 + WHERE 1=1
56 + AND {$wpdb->posts}.post_type = %s
57 + AND {$wpdb->postmeta}.meta_key = %s
58 + ORDER BY {$wpdb->posts}.post_date DESC",
59 + $postType,
60 + '_ultimate_store_kit_template_type'
61 + ));
62 + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
52 63
53 64 foreach ($query as $q) {
54 - if (!$q->template_type) {
65 + if (! $q->template_type) {
55 66 continue;
56 67 }
57 68
58 - $optionKey = Meta::TEMPLATE_ID . $q->template_type;
59 -
60 69 if ($q->post_status == 'publish') {
61 - update_option($optionKey, $q->ID);
70 + Meta::update_template_option($q->template_type, $q->ID);
62 71 } else {
63 - delete_option($optionKey, $q->ID);
72 + Meta::delete_template_option($q->template_type);
64 73 }
65 74 }
66 75 }
67 76
@@ -70,9 +79,9 @@
70 79 return;
71 80 }
72 81
73 82 if ($template = get_post_meta($postId, Meta::TEMPLATE_TYPE, true)) {
74 - delete_option(Meta::TEMPLATE_ID . $template);
83 + Meta::delete_template_option($template);
75 84 }
76 85 }
77 86
78 87 public function post_row_actions_filter($actions, $post) {
@@ -82,30 +91,49 @@
82 91 if ($typenow !== Meta::POST_TYPE) {
83 92 return $actions;
84 93 }
85 94
86 - if (isset($actions['edit_with_elementor'])) {
87 - unset($actions['edit_with_elementor']);
88 - }
95 + if (isset($actions['edit_with_elementor'])) {
96 + unset($actions['edit_with_elementor']);
97 + }
89 98
90 - if (get_post_meta($post->ID, Meta::EDIT_WITH, true) == 'gutenberg') {
91 - $actions['usk_edit_with_gutenberg'] = sprintf(
92 - '<a href="%1$s">%2$s</a>',
93 - add_query_arg(['post' => $post->ID, 'action' => 'edit'], admin_url('post.php')),
94 - esc_html__('Edit with Gutenberg', 'ultimate-store-kit')
95 - );
99 + if (get_post_meta($post->ID, Meta::EDIT_WITH, true) == 'gutenberg') {
100 + $actions['usk_edit_with_gutenberg'] = sprintf(
101 + '<a href="%1$s">%2$s</a>',
102 + add_query_arg(['post' => $post->ID, 'action' => 'edit'], admin_url('post.php')),
103 + esc_html__('Edit with Gutenberg', 'ultimate-store-kit')
104 + );
105 + }
96 106
97 - }
107 + if (get_post_meta($post->ID, Meta::EDIT_WITH, true) == 'elementor') {
108 + $actions['usk_edit_with_elementor'] = sprintf(
109 + '<a href="%1$s">%2$s</a>',
110 + add_query_arg(
111 + [
112 + 'post' => $post->ID,
113 + 'action' => 'elementor',
114 + 'usk-template' => 1
115 + ],
116 + admin_url('post.php')
117 + ),
118 + esc_html__('Edit with Elementor', 'ultimate-store-kit')
119 + );
120 + } else {
121 + // Always offer Elementor edit option regardless of stored preference
122 + $actions['usk_edit_with_elementor'] = sprintf(
123 + '<a href="%1$s">%2$s</a>',
124 + add_query_arg(
125 + [
126 + 'post' => $post->ID,
127 + 'action' => 'elementor',
128 + 'usk-template' => 1
129 + ],
130 + admin_url('post.php')
131 + ),
132 + esc_html__('Edit with Elementor', 'ultimate-store-kit')
133 + );
134 + }
98 135
99 - if (get_post_meta($post->ID, Meta::EDIT_WITH, true) == 'elementor') {
100 - $actions['usk_edit_with_elementor'] = sprintf(
101 - '<a href="%1$s">%2$s</a>',
102 - add_query_arg(['post' => $post->ID, 'action' => 'elementor','usk-template' => 1], admin_url('post.php')),
103 - esc_html__('Edit with Elementor', 'ultimate-store-kit')
104 - );
105 -
106 - }
107 -
108 136 $editActionLink = sprintf(
109 137 '<a href="%1$s" data-id="%2$s" >%3$s</a>',
110 138 'javascript:void(0)',
111 139 $post->ID,
@@ -119,9 +147,12 @@
119 147 return array_slice($actions, 0, 1, true) + ['usk-edit-action' => $editActionLink] + array_slice($actions, 1, null, true);
120 148 }
121 149
122 150 public function set_post_columns($columns) {
123 - return array_slice($columns, 0, 2, true) + ['template_type' => 'Type', 'is_enabled' => 'Status'] + array_slice($columns, 2, null, true);
151 + return array_slice($columns, 0, 2, true) + [
152 + 'template_type' => esc_html__('Type', 'ultimate-store-kit'),
153 + 'is_enabled' => esc_html__('Status', 'ultimate-store-kit')
154 + ] + array_slice($columns, 2, null, true);
124 155 }
125 156
126 157 public function set_custom_column_value($column, $post_id) {
127 158 $templateType = get_post_meta(
@@ -132,14 +163,17 @@
132 163
133 164 switch ($column) {
134 165 case 'template_type':
135 166 $postType = Builder_Template_Helper::getTemplatePostTypeByIndex($templateType);
136 - $postTypeLabel = isset($postType->name) ? ' <strong>-- ' . ucwords($postType->name) . '</strong>' : '';
137 -
138 - echo Builder_Template_Helper::getTemplateByIndex($templateType) . $postTypeLabel;
167 + echo esc_html(Builder_Template_Helper::getTemplateByIndex($templateType));
168 + if (isset($postType->name)) {
169 + echo ' <strong>-- ' . esc_html(ucwords($postType->name)) . '</strong>';
170 + }
139 171 break;
140 172 case 'is_enabled':
141 - echo (Builder_Template_Helper::getTemplateId($templateType) == $post_id ? 'Active' : 'Inactive');
173 + echo Builder_Template_Helper::getTemplateId($templateType) == $post_id
174 + ? esc_html__('Active', 'ultimate-store-kit')
175 + : esc_html__('Inactive', 'ultimate-store-kit');
142 176 break;
143 177 }
144 178 }
145 179
@@ -149,17 +183,18 @@
149 183 if ($typenow !== Meta::POST_TYPE) {
150 184 return;
151 185 }
152 186
153 - $selected = isset($_GET['type']) ? sanitize_key($_GET['type']) : '';
187 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list-table filter; the value only re-selects the current dropdown option.
188 + $selected = isset($_GET['type']) ? sanitize_text_field(wp_unslash($_GET['type'])) : '';
154 189 ?>
155 190 <select name="type" id="type">
156 191 <option value="all" <?php
157 192 selected('all', $selected); ?>><?php
158 - esc_html_e(
159 - 'Template Type ',
160 - 'ultimate-store-kit'
161 - ); ?></option>
193 + esc_html_e(
194 + 'Template Type ',
195 + 'ultimate-store-kit'
196 + ); ?></option>
162 197 <?php
163 198 $templates = Builder_Template_Helper::templateForSelectDropdown();
164 199 // It is single
165 200 if (count($templates) == 1) {
@@ -169,9 +204,9 @@
169 204 $selectValue = "{$templateKey}_$key";
170 205 ?>
171 206 <option value="<?php
172 207 echo esc_attr($selectValue) ?>"><?php
173 - echo wp_kses_post($item) ?></option>
208 + echo wp_kses_post($item) ?></option>
174 209 <?php
175 210 endforeach;
176 211 }
177 212
@@ -185,17 +220,17 @@
185 220 if (is_array($items)) {
186 221 ?>
187 222 <optgroup label="<?php
188 223 echo esc_attr($label) ?>"><?php
189 - foreach ($items as $key => $item) :
190 - $itemValue = "{$keys}_$key"
191 - ?>
224 + foreach ($items as $key => $item) :
225 + $itemValue = "{$keys}_$key"
226 + ?>
192 227 <option value="<?php
193 - echo esc_attr($itemValue) ?>" <?php
194 - selected($key, $selected); ?>><?php
195 - echo wp_kses_post($item) ?></option>
228 + echo esc_attr($itemValue) ?>" <?php
229 + selected($key, $selected); ?>><?php
230 + echo wp_kses_post($item) ?></option>
196 231 <?php
197 - endforeach;
232 + endforeach;
198 233 ?>
199 234 </optgroup>
200 235 <?php
201 236 }
@@ -212,8 +247,9 @@
212 247 if ($typenow !== Meta::POST_TYPE) {
213 248 return;
214 249 }
215 250
251 + // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.DB.SlowDBQuery -- Admin list-table filter; the value only narrows the query that is already running.
216 252 if (
217 253 'edit.php' == $pagenow
218 254 && isset($_GET['type'])
219 255 && $_GET['type'] != ''
@@ -218,24 +254,52 @@
218 254 && isset($_GET['type'])
219 255 && $_GET['type'] != ''
220 256 && $_GET['type'] != 'all'
221 257 ) {
222 - $query->query_vars['meta_key'] = Meta::TEMPLATE_TYPE;
223 - $query->query_vars['meta_value'] = sanitize_key($_GET['type']);
224 - $query->query_vars['meta_compare'] = '=';
258 + $requested_type = sanitize_text_field(wp_unslash($_GET['type']));
259 +
260 + if (Builder_Template_Helper::getTemplateByIndex($requested_type)) {
261 + $query->query_vars['meta_key'] = Meta::TEMPLATE_TYPE;
262 + $query->query_vars['meta_value'] = $requested_type;
263 + $query->query_vars['meta_compare'] = '=';
264 + }
225 265 }
266 + // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.DB.SlowDBQuery
226 267 }
227 268
228 269
229 270 public function create_builder_template() {
230 - parse_str($_POST['data'], $data);
231 271
232 - $templateId = isset($data['template_id']) ? trim($data['template_id']) : '';
233 - $name = isset($data['template_name']) ? trim($data['template_name']) : '';
234 - $type = isset($data['template_type']) ? trim($data['template_type']) : '';
235 - $editWith = isset($data['edit_with']) ? trim($data['edit_with']) : 'elementor'; //gutenberg
272 + if (! current_user_can('manage_options')) {
273 + wp_send_json_error(['success' => false, 'errors_arr' => ['permission' => 'Permission denied']], 403);
274 + }
275 +
276 + $data = [];
277 + // The form arrives as one serialized string, so it is unslashed before parsing
278 + // and every field is sanitized individually below — parse_str itself does not
279 + // sanitize anything.
280 + if (isset($_POST['data'])) {
281 + parse_str(wp_unslash($_POST['data']), $data); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Serialized form string; every parsed field is sanitized individually below.
282 + }
283 +
284 + $nonce = isset($data['nonce']) ? sanitize_text_field($data['nonce']) : '';
285 +
286 + if (! wp_verify_nonce($nonce, 'usk-builder')) {
287 + wp_send_json_error(['success' => false, 'errors_arr' => ['nonce' => 'Invalid nonce']], 403);
288 + }
289 +
290 + $templateId = isset($data['template_id']) ? absint($data['template_id']) : 0;
291 + $name = isset($data['template_name']) ? sanitize_text_field($data['template_name']) : '';
292 + $type = isset($data['template_type']) ? sanitize_text_field($data['template_type']) : '';
293 + $editWith = isset($data['edit_with']) ? sanitize_key($data['edit_with']) : 'elementor'; //gutenberg
236 294 $isEnabled = (isset($data['template_status']) && $data['template_status']) == 1 ? 1 : 0;
237 295
296 + // Only these two editors are ever handled below, and the value ends up in a
297 + // redirect URL, so anything else falls back to the default.
298 + if (! in_array($editWith, ['elementor', 'gutenberg'], true)) {
299 + $editWith = 'elementor';
300 + }
301 +
238 302 $errors = [];
239 303
240 304 if (empty($name)) {
241 305 $errors['template_name'] = 'Field is required';
@@ -243,9 +307,9 @@
243 307
244 308 if (empty($type)) {
245 309 $errors['template_type'] = 'Field is required';
246 310 } else {
247 - if (!Builder_Template_Helper::getTemplateByIndex($type)) {
311 + if (! Builder_Template_Helper::getTemplateByIndex($type)) {
248 312 $errors['template_type'] = 'Invalid section';
249 313 }
250 314 }
251 315
@@ -275,24 +339,23 @@
275 339 }
276 340
277 341 $post_id = wp_insert_post($page_data);
278 342
279 - $enabledTemplate = strtolower(Meta::TEMPLATE_ID . $type);
280 343 if ($isEnabled == 1) {
281 - update_option($enabledTemplate, $post_id);
344 + Meta::update_template_option($type, $post_id);
282 345 } else {
283 - if (get_option($enabledTemplate) == $post_id) {
284 - delete_option($enabledTemplate);
346 + if (Meta::get_template_option($type) == $post_id) {
347 + Meta::delete_template_option($type);
285 348 }
286 349 }
287 350
288 351
289 352 if ($editWith == 'elementor') {
290 - if (!get_post_meta($post_id, '_elementor_data', [])) {
353 + if (! get_post_meta($post_id, '_elementor_data')) {
291 354 update_post_meta($post_id, '_elementor_data', []);
292 355 }
293 356
294 - if (!$templateId) {
357 + if (! $templateId) {
295 358 update_post_meta($post_id, '_elementor_data', []);
296 359 }
297 360 update_post_meta($post_id, '_wp_page_template', 'elementor_header_footer');
298 361 update_post_meta($post_id, '_elementor_edit_mode', 'builder');
@@ -314,19 +377,29 @@
314 377 wp_send_json_success(['success' => true, 'redirect' => $url]);
315 378 }
316 379
317 380 public function get_builder_template_action() {
318 - if (isset($_REQUEST['template_id']) && !empty($_REQUEST['template_id'])) {
319 - $templateId = $_REQUEST['template_id'];
381 +
382 + if (! current_user_can('manage_options')) {
383 + wp_send_json_error(['success' => false, 'errors_arr' => ['permission' => 'Permission denied']], 403);
384 + }
385 +
386 + $nonce = isset($_REQUEST['nonce']) ? sanitize_text_field(wp_unslash($_REQUEST['nonce'])) : '';
387 +
388 + if (! wp_verify_nonce($nonce, 'ultimate_store_kit_builder_nonce')) {
389 + wp_send_json_error(['success' => false, 'errors_arr' => ['nonce' => 'Invalid nonce']], 403);
390 + }
391 +
392 + if (isset($_REQUEST['template_id']) && ! empty($_REQUEST['template_id'])) {
393 + $templateId = absint($_REQUEST['template_id']);
320 394 $templateData = get_post($templateId);
321 395
322 - if ($templateData) {
396 + if ($templateData && $templateData->post_type === Meta::POST_TYPE) {
323 397 $meta = get_post_meta($templateData->ID);
324 398
325 399
326 - $templateType = isset($meta[Meta::TEMPLATE_TYPE][0]) ? $meta[Meta::TEMPLATE_TYPE][0] : '';
327 - $enabledTemplate = strtolower(Meta::TEMPLATE_ID . $templateType);
328 - $enabledTemplate = get_option($enabledTemplate);
400 + $templateType = isset($meta[Meta::TEMPLATE_TYPE][0]) ? $meta[Meta::TEMPLATE_TYPE][0] : '';
401 + $enabledTemplate = Meta::get_template_option($templateType);
329 402
330 403 wp_send_json_success([
331 404 'id' => $templateData->ID,
332 405 'name' => $templateData->post_title,
@@ -334,8 +407,10 @@
334 407 'status' => is_numeric($enabledTemplate) ? 1 : 0,
335 408 ]);
336 409 }
337 410 }
411 +
412 + wp_send_json_error(['success' => false, 'errors_arr' => ['template' => 'Template not found']], 404);
338 413 }
339 414
340 415 public function add_modal_html($hook_suffix) {
341 416 require_once 'modal/modal.php';
@@ -345,10 +420,17 @@
345 420 if (in_array($hook_suffix, ['edit.php', 'post-new.php'])) {
346 421 $screen = get_current_screen();
347 422
348 423 if (is_object($screen) && Meta::POST_TYPE == $screen->post_type) {
349 - wp_enqueue_style('ultimate-store-kit-builder', BDTUSK_ADM_ASSETS_URL . 'css/ultimate-builder.css', [], BDTUSK_VER);
350 - wp_enqueue_script('ultimate-store-kit-builder', BDTUSK_ADM_ASSETS_URL . 'js/ultimate-builder.js', ['jquery'], BDTUSK_VER);
424 + wp_enqueue_style('ultimate-store-kit-builder', BDTUSK_ASSETS_URL . 'admin/others/css/ultimate-builder.css', [], BDTUSK_VER);
425 + wp_enqueue_script('ultimate-store-kit-builder', BDTUSK_ASSETS_URL . 'admin/others/js/ultimate-builder.js', ['jquery', 'wp-i18n'], BDTUSK_VER, true);
426 + wp_set_script_translations('ultimate-store-kit-builder', 'ultimate-store-kit');
427 +
428 + wp_localize_script('ultimate-store-kit-builder', 'UltimateStoreKitConfigBuilder', [
429 + 'ajaxurl' => admin_url('admin-ajax.php'),
430 + 'nonce' => wp_create_nonce('ultimate_store_kit_builder_nonce'),
431 + 'resturl' => rest_url('usk/v1/'),
432 + ]);
351 433 }
352 434 }
353 435 }
354 436
@@ -354,12 +436,12 @@
354 436
355 437
356 438 public function add_admin_menu() {
357 439 add_submenu_page(
358 - 'ultimate_store_kit_options',
440 + 'ultimate-store-kit',
359 441 esc_html__('Template Builder', 'ultimate-store-kit'),
360 442 esc_html__('Template Builder', 'ultimate-store-kit'),
361 - 'edit_pages',
443 + 'manage_options',
362 444 'edit.php?post_type=' . Meta::POST_TYPE
363 445 );
364 446 }
365 447
@@ -381,32 +463,54 @@
381 463 'not_found_in_trash' => __('No Template found in Trash.', 'ultimate-store-kit'),
382 464 ];
383 465
384 466 $args = [
385 - 'labels' => $labels,
386 - 'description' => __('Description.', 'ultimate-store-kit'),
387 - 'taxonomies' => [],
388 - 'hierarchical' => false,
389 - 'public' => true,
390 - 'show_in_menu' => false,
391 - 'show_ui' => true,
392 - 'show_in_admin_bar' => true,
393 - 'menu_position' => null,
394 - 'menu_icon' => null,
395 - 'publicly_queryable' => true,
396 - 'supports' => ['title', 'editor', 'elementor'],
397 - 'exclude_from_search' => true,
398 - 'has_archive' => false,
399 - 'query_var' => true,
400 - 'can_export' => true,
401 - 'rewrite' => false,
402 - 'show_in_nav_menus' => false,
403 - 'capability_type' => 'post',
467 + 'labels' => $labels,
468 + 'description' => __('Description.', 'ultimate-store-kit'),
469 + 'taxonomies' => [],
470 + 'hierarchical' => false,
471 + 'public' => true,
472 + 'show_in_menu' => false,
473 + 'show_ui' => true,
474 + 'show_in_admin_bar' => true,
475 + 'menu_position' => null,
476 + 'menu_icon' => null,
477 + 'publicly_queryable' => true,
478 + 'supports' => ['title', 'editor', 'elementor'],
479 + 'exclude_from_search' => true,
480 + 'has_archive' => false,
481 + 'query_var' => true,
482 + 'can_export' => true,
483 + 'rewrite' => false,
484 + 'show_in_nav_menus' => false,
485 + 'capability_type' => 'post',
404 486 // 'rest_base' => $this->getPostType(),
405 487 // 'register_meta_box_cb' => [ $this, 'register_meta_box_cb' ],
406 488 ];
407 489
408 490 register_post_type(Meta::POST_TYPE, $args);
491 +
492 + // Fix WPML integration with Elementor
493 + if (function_exists('icl_object_id')) {
494 + add_filter('wpml_pb_elementor_get_data', [$this, 'fix_wpml_elementor_data'], 10, 2);
495 + }
496 + }
497 +
498 + /**
499 + * Simple fix for WPML and Elementor integration
500 + */
501 + public function fix_wpml_elementor_data() {
502 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reads the post being edited so WPML/Elementor metadata can be normalised.
503 + $post_id = isset($_REQUEST['post']) ? absint($_REQUEST['post']) : 0;
504 +
505 + if ($post_id && get_post_type($post_id) === Meta::POST_TYPE) {
506 + $meta_data = get_post_meta($post_id, '_elementor_data', true);
507 +
508 + // If metadata exists but is in array format, convert it to JSON string
509 + if (is_array($meta_data)) {
510 + update_post_meta($post_id, '_elementor_data', wp_json_encode($meta_data));
511 + }
512 + }
409 513 }
410 514 }
411 515
412 516 Builder_Cpt::instance()->init_hooks();