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/wishlist-compare.php +182 -72 2.0.23.1.4 View file →
@@ -1,29 +1,28 @@
1 1 <?php
2 2
3 3 namespace UltimateStoreKit;
4 4
5 -if ( ! defined( 'ABSPATH' ) ) {
5 +if (! defined('ABSPATH')) {
6 6 exit;
7 7 }
8 8
9 9 // Exit if accessed directly
10 10
11 -if ( ! function_exists( 'is_plugin_active' ) ) {
12 - include_once ABSPATH . 'wp-admin/includes/plugin.php';
13 -}
14 -
15 11 final class WishlistCompare {
16 12 public function __construct() {
17 - add_action( 'wp_ajax_usk_add_to_wishlist', [ $this, 'usk_add_to_wishlist' ] );
18 - add_action( 'wp_ajax_nopriv_usk_add_to_wishlist', [ $this, 'usk_add_to_wishlist' ] );
13 + add_action('wp_ajax_usk_add_to_wishlist', [$this, 'usk_add_to_wishlist']);
14 + add_action('wp_ajax_nopriv_usk_add_to_wishlist', [$this, 'usk_add_to_wishlist']);
19 15
20 - add_action( 'wp_ajax_usk_add_to_compare_products', [ $this, 'usk_add_to_compare_products' ] );
21 - add_action( 'wp_ajax_nopriv_usk_add_to_compare_products', [ $this, 'usk_add_to_compare_products' ] );
16 + add_action('wp_ajax_usk_remove_wishlist', [$this, 'usk_remove_wishlist']);
17 + add_action('wp_ajax_nopriv_usk_remove_wishlist', [$this, 'usk_remove_wishlist']);
22 18
23 - add_action( 'wp_ajax_usk_remove_from_compare_products', [ $this, 'usk_remove_from_compare_products' ] );
24 - add_action( 'wp_ajax_nopriv_usk_remove_from_compare_products', [ $this, 'usk_remove_from_compare_products' ] );
19 + add_action('wp_ajax_usk_add_to_compare_products', [$this, 'usk_add_to_compare_products']);
20 + add_action('wp_ajax_nopriv_usk_add_to_compare_products', [$this, 'usk_add_to_compare_products']);
25 21
22 + add_action('wp_ajax_usk_remove_from_compare_products', [$this, 'usk_remove_from_compare_products']);
23 + add_action('wp_ajax_nopriv_usk_remove_from_compare_products', [$this, 'usk_remove_from_compare_products']);
24 +
26 25 //wishlist
27 26 // add_action('woocommerce_account_wishlist_endpoint', [$this, 'usk_wishlist_content']);
28 27 // add_filter('woocommerce_account_menu_items', [$this, 'usk_wishlist_link_my_account']);
29 28 // add_filter('query_vars', [$this, 'usk_wishlist_query_vars'], 0);
@@ -29,67 +28,152 @@
29 28 // add_filter('query_vars', [$this, 'usk_wishlist_query_vars'], 0);
30 29 // add_action('init', [$this, 'usk_add_rewrite_flash_rules_endpoint']);
31 30 }
32 31
32 + /**
33 + * CSRF check for the wishlist/compare endpoints.
34 + *
35 + * A logged-out visitor's list lives entirely in their own cookie, and their
36 + * pages are routinely served from a full-page cache where an embedded nonce
37 + * would already be stale — enforcing one there breaks the feature without
38 + * protecting any server-side state. A logged-in request writes to user meta,
39 + * so that path gets the check.
40 + */
41 + private function verify_request() {
42 + if (! is_user_logged_in()) {
43 + return;
44 + }
45 +
46 + check_ajax_referer('usk_wishlist_compare', 'nonce');
47 + }
48 +
49 + // Every handler below calls verify_request() first, which PHPCS cannot follow.
50 + // See its docblock for why the logged-out path is deliberately exempt.
51 + // phpcs:disable WordPress.Security.NonceVerification.Missing
52 +
33 53 public function usk_add_to_wishlist() {
34 - $response = [
54 + $this->verify_request();
55 +
56 + $response = [
35 57 'status' => 0,
36 - 'message' => __( 'Unauthorized!', 'ultimate-store-kit' ),
58 + 'message' => __('Unauthorized!', 'ultimate-store-kit'),
37 59 ];
38 60
39 - if ( ! isset( $_POST['product_id'] ) ) {
40 - $response['message'] = __( 'No product selected!', 'ultimate-store-kit' );
41 - wp_send_json( $response );
61 + if (! isset($_POST['product_id'])) {
62 + $response['message'] = __('No product selected!', 'ultimate-store-kit');
63 + wp_send_json($response);
42 64 }
43 65
44 - $product_id = isset( $_POST['product_id'] ) ? sanitize_text_field( $_POST['product_id'] ) : '';
66 + $product_id = isset($_POST['product_id']) ? absint($_POST['product_id']) : 0;
45 67
46 68 $user_id = get_current_user_id();
47 - $wishlist = ultimate_store_kit_get_wishlist( $user_id );
69 + $wishlist = ultimate_store_kit_get_wishlist($user_id);
48 70
49 - $wishlistCounter = count( $wishlist );
71 + $wishlistCounter = count($wishlist);
50 72
51 - if ( ( $key = array_search( $product_id, $wishlist ) ) !== false ) {
73 + if (($key = array_search($product_id, $wishlist)) !== false) {
52 74 $response['action'] = 'removed';
53 75 $response['count'] = --$wishlistCounter;
54 - unset( $wishlist[ $key ] );
76 + unset($wishlist[$key]);
55 77 } else {
78 + // Only gate additions. A removal just drops an id the visitor already
79 + // holds, so it must keep working even if the product was since unpublished.
80 + if (! ultimate_store_kit_is_public_product($product_id)) {
81 + $response['message'] = __('Invalid product!', 'ultimate-store-kit');
82 + wp_send_json($response);
83 + }
84 +
85 + if ($wishlistCounter >= ultimate_store_kit_get_list_item_limit()) {
86 + $response['message'] = __('Wishlist is full!', 'ultimate-store-kit');
87 + wp_send_json($response);
88 + }
89 +
56 90 $response['action'] = 'added';
57 91 $response['count'] = ++$wishlistCounter;
58 92 $wishlist[] = $product_id;
59 93 }
60 94
61 - $wishlist = array_unique( $wishlist );
95 + $wishlist = array_unique($wishlist);
62 96
63 97 // update wishlist
64 - $this->ultimate_store_kit_set_wishlist( $wishlist, $user_id );
98 + $this->ultimate_store_kit_set_wishlist($wishlist, $user_id);
65 99
66 100 // send response
67 101 $response['status'] = 1;
68 102
69 - if ( $response['action'] == 'added' ) {
70 - $response['message'] = __( "Wishlist item Added!", "ultimate-store-kit" );
71 - wp_send_json( $response );
103 + if ($response['action'] == 'added') {
104 + $response['message'] = __("Wishlist item Added!", "ultimate-store-kit");
105 + wp_send_json($response);
72 106 } else {
73 - $response['message'] = __( "Add To Wishlist", "ultimate-store-kit" );
74 - wp_send_json( $response );
107 + $response['message'] = __("Add To Wishlist", "ultimate-store-kit");
108 + wp_send_json($response);
75 109 }
76 110 }
77 111
78 - public function ultimate_store_kit_set_wishlist( $wishlist, $user_id = 0 ) {
79 - $_wishlist_key = '_ultimate_store_kit_wishlist';
80 - $user_id = get_current_user_id();
81 - $existing_wishlist = ultimate_store_kit_get_wishlist( $user_id );
112 + /**
113 + * Remove a single product from the wishlist.
114 + *
115 + * Deliberately idempotent rather than a toggle: the remove control is bound by
116 + * more than one script, so a click can fire this twice. A toggle would remove
117 + * the product and then immediately put it back.
118 + */
119 + public function usk_remove_wishlist() {
120 + $this->verify_request();
82 121
83 - $wishlist = array_unique( array_merge( $existing_wishlist, $wishlist ) );
122 + $response = [
123 + 'status' => 0,
124 + 'message' => __('Unauthorized!', 'ultimate-store-kit'),
125 + ];
84 126
85 - setcookie( $_wishlist_key, serialize( $wishlist ), time() + MONTH_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
127 + if (! isset($_POST['product_id'])) {
128 + $response['message'] = __('No product selected!', 'ultimate-store-kit');
129 + wp_send_json($response);
130 + }
131 +
132 + $product_id = absint($_POST['product_id']);
133 + $user_id = get_current_user_id();
134 + $wishlist = ultimate_store_kit_get_wishlist($user_id);
135 +
136 + if (($key = array_search($product_id, $wishlist)) !== false) {
137 + unset($wishlist[$key]);
138 + }
139 +
140 + $this->ultimate_store_kit_set_wishlist($wishlist, $user_id);
141 +
142 + // Report the post-condition, not what this particular call changed, so a
143 + // duplicate request still tells the UI the row is gone.
144 + $response['status'] = 1;
145 + $response['action'] = 'removed';
146 + $response['count'] = count($wishlist);
147 + $response['message'] = __('Wishlist item removed!', 'ultimate-store-kit');
148 +
149 + wp_send_json($response);
86 150 }
87 151
152 + /**
153 + * Persist the wishlist.
154 + *
155 + * @param array $wishlist Full list to store — this replaces what is stored.
156 + * @param int $user_id Unused; kept for signature compatibility. The wishlist
157 + * is cookie-backed for every visitor, logged in or not.
158 + */
159 + public function ultimate_store_kit_set_wishlist($wishlist, $user_id = 0) {
160 + $_wishlist_key = '_ultimate_store_kit_wishlist';
161 +
162 + // Straight write, not a merge. This previously merged the incoming list back
163 + // into the stored one, which silently undid every removal — nothing could
164 + // ever leave a wishlist. Reindexed because unset() leaves a gap, and a gapped
165 + // array json_encodes to an object that the getter then discards.
166 + $wishlist = array_values(array_unique($wishlist));
167 +
168 + setcookie($_wishlist_key, wp_json_encode($wishlist), time() + MONTH_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
169 + }
170 +
88 171 public function get_compare_product_page_id() {
89 - if ( $comparePage = ultimate_store_kit_compare_product_page() ) {
90 - return $comparePage->ID;
91 - }
172 + // ultimate_store_kit_compare_product_page() already returns an int post id.
173 + // Reading ->ID off it warned under PHP 8 and evaluated to null, so the
174 + // "Added" response never carried a compare page URL.
175 + return ultimate_store_kit_compare_product_page();
92 176 }
93 177
94 178
95 179 //======================================
@@ -95,86 +179,112 @@
95 179 //======================================
96 180 //=========COMPARE PRODUCTS=============
97 181 //======================================
98 182 public function usk_add_to_compare_products() {
99 - $response = [
183 + $this->verify_request();
184 +
185 + $response = [
100 186 'status' => 0,
101 - 'message' => __( 'Unauthorized!', 'usk' ),
187 + 'message' => __('Unauthorized!', 'ultimate-store-kit'),
102 188 ];
103 189
104 - if ( ! isset( $_POST['product_id'] ) ) {
105 - $response['message'] = __( 'No product selected!', 'usk' );
106 - wp_send_json( $response );
190 + if (! isset($_POST['product_id'])) {
191 + $response['message'] = __('No product selected!', 'ultimate-store-kit');
192 + wp_send_json($response);
107 193 }
108 194
195 + $product_id = absint($_POST['product_id']);
196 +
197 + if (! ultimate_store_kit_is_public_product($product_id)) {
198 + $response['message'] = __('Invalid product!', 'ultimate-store-kit');
199 + wp_send_json($response);
200 + }
201 +
109 202 $user_id = get_current_user_id();
110 - $compare_products = usk_get_compare_products( $user_id );
203 + $compare_products = ultimate_store_kit_get_compare_products($user_id);
111 204
205 + if (! is_array($compare_products)) {
206 + $compare_products = [];
207 + }
208 +
209 + if (count($compare_products) >= ultimate_store_kit_get_list_item_limit() && ! in_array($product_id, $compare_products)) {
210 + $response['message'] = __('Compare list is full!', 'ultimate-store-kit');
211 + wp_send_json($response);
212 + }
213 +
112 214 // count compare products
113 - if ( is_array( $compare_products ) ) {
114 - $response['count'] = count( $compare_products ) + 1;
115 - }
215 + $response['count'] = count($compare_products) + 1;
116 216
117 217 //add to compare products
118 218 $response['action'] = 'added';
119 - $compare_products[] = $_POST['product_id'];
219 + $compare_products[] = $product_id;
120 220
121 - $compare_products = array_unique( $compare_products );
221 + $compare_products = array_unique($compare_products);
122 222
123 223 // update compare_productsusk_add_to_compare_products
124 - $this->ultimate_store_kit_set_compare_products( $compare_products, $user_id );
224 + $this->ultimate_store_kit_set_compare_products($compare_products, $user_id);
125 225
126 226 // send response
127 227 $response['status'] = 1;
128 - if ( $response['action'] == 'added' ) {
129 - $response['message'] = __( "Added", "ultimate-store-kit" );
228 + if ($response['action'] == 'added') {
229 + $response['message'] = __("Added", "ultimate-store-kit");
130 230 $response['url'] = '';
131 - if ( $pageId = $this->get_compare_product_page_id() ) {
132 - $response['url'] = get_permalink( $pageId );
231 + if ($pageId = $this->get_compare_product_page_id()) {
232 + $response['url'] = get_permalink($pageId);
133 233 }
134 234
135 - wp_send_json( $response );
235 + wp_send_json($response);
136 236 } else {
137 - $response['message'] = __( "Compare", "ultimate-store-kit" );
138 - wp_send_json( $response );
237 + $response['message'] = __("Compare", "ultimate-store-kit");
238 + wp_send_json($response);
139 239 }
140 240 }
141 241 public function usk_remove_from_compare_products() {
142 - $response = [
242 + $this->verify_request();
243 +
244 + $response = [
143 245 'status' => 0,
144 - 'message' => __( 'Unauthorized!', 'ultimate-store-kit' ),
246 + 'message' => __('Unauthorized!', 'ultimate-store-kit'),
145 247 ];
146 - if ( ! isset( $_POST['product_id'] ) ) {
147 - $response['message'] = __( 'No product selected!', 'ultimate-store-kit' );
148 - wp_send_json( $response );
248 + if (! isset($_POST['product_id'])) {
249 + $response['message'] = __('No product selected!', 'ultimate-store-kit');
250 + wp_send_json($response);
149 251 }
252 + $product_id = absint($_POST['product_id']);
150 253 $user_id = get_current_user_id();
151 - $compare_products = usk_get_compare_products( $user_id );
254 + $compare_products = ultimate_store_kit_get_compare_products($user_id);
152 255
153 256 //add remove from compare products
154 - if ( ( $key = array_search( $_POST['product_id'], $compare_products ) ) !== false ) {
257 + if (($key = array_search($product_id, $compare_products)) !== false) {
155 258 $response['action'] = 'removed';
156 - unset( $compare_products[ $key ] );
259 + unset($compare_products[$key]);
157 260 }
158 - $compare_products = array_unique( $compare_products );
159 261
262 + // Reindex: unset() leaves a gap, and a gapped array json_encodes to an
263 + // object, which breaks the cookie read back in ultimate_store_kit_get_compare_products().
264 + $compare_products = array_values(array_unique($compare_products));
265 +
160 266 // update compare_products
161 - $this->ultimate_store_kit_set_compare_products( $compare_products, $user_id );
267 + $this->ultimate_store_kit_set_compare_products($compare_products, $user_id);
162 268
163 269 // send response
164 270 $response['status'] = 1;
165 - $response['message'] = sprintf( __( 'compare products item %s!', 'ultimate-store-kit' ), $response['action'] );
166 - wp_send_json( $response );
271 + /* translators: %s is the action performed on the compared product */
272 + $response['message'] = sprintf(__('Compared product item: %s.', 'ultimate-store-kit'), $response['action']);
273 +
274 + wp_send_json($response);
167 275 }
168 - public function ultimate_store_kit_set_compare_products( $compare_products, $user_id = 0 ) {
276 + public function ultimate_store_kit_set_compare_products($compare_products, $user_id = 0) {
169 277 $_compare_products_key = '_ultimate_store_kit_compare_products';
170 278 $_compare_products = [];
171 279
172 - if ( $user_id != 0 ) {
173 - update_user_meta( $user_id, $_compare_products_key, $compare_products );
280 + if ($user_id != 0) {
281 + update_user_meta($user_id, $_compare_products_key, $compare_products);
174 282 } else {
175 - setcookie( $_compare_products_key, serialize( $compare_products ), time() + MONTH_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
283 + setcookie($_compare_products_key, json_encode($compare_products), time() + MONTH_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
176 284 }
177 285 }
286 +
287 + // phpcs:enable WordPress.Security.NonceVerification.Missing
178 288 }
179 289
180 290 new WishlistCompare();