PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / ajax / settings.php

settings.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/ajax/settings.php

504 lines 21.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace StoreEngine\Ajax;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 use StoreEngine\Admin\Settings\Base as BaseSettings;
10 use StoreEngine\Classes\AbstractAjaxHandler;
11 use StoreEngine\Classes\Exceptions\StoreEngineException;
12 use StoreEngine\Utils\CheckoutFields;
13 use StoreEngine\Utils\Geolocation;
14 use StoreEngine\Utils\Helper;
15 use WP_Error;
16
17 class Settings extends AbstractAjaxHandler {
18 protected array $payment_fields;
19 protected array $settings_fields;
20
21 public function __construct() {
22 $this->load_fields();
23 $this->actions = [
24 'update_base_settings' => [
25 'capability' => 'manage_options',
26 'callback' => [ $this, 'update_base_settings' ],
27 'fields' => $this->settings_fields,
28 ],
29 'update_payments_settings' => [
30 'capability' => 'manage_options',
31 'callback' => [ $this, 'update_payments_settings' ],
32 'fields' => [
33 'payments' => $this->payment_fields,
34 ],
35 ],
36 'verify_payment_method_config' => [
37 'capability' => 'manage_options',
38 'callback' => [ $this, 'verify_payment_method_config' ],
39 'fields' => [
40 'method' => 'string',
41 'config' => array_merge( ...array_values( $this->payment_fields ) ),
42 ],
43 ],
44 ];
45
46 // This handler is constructed on `plugins_loaded` (StoreEngine boots
47 // there), which is before `init`. Building the payment schema instantiates
48 // every gateway, and their setup() calls __() — tripping WP 6.7+'s
49 // `_load_textdomain_just_in_time` notice. So load_fields() skips the
50 // payment schema until `init`; on an ajax request rebuild it then, which
51 // still runs before the wp_ajax_* action dispatches.
52 if ( wp_doing_ajax() && ! did_action( 'init' ) ) {
53 add_action( 'init', [ $this, 'refresh_payment_fields' ] );
54 }
55 }
56
57 /**
58 * Rebuild the payment field schema once `init` has fired and translations
59 * are safe to load. See __construct() for why this can't run at construction.
60 */
61 public function refresh_payment_fields(): void {
62 $this->payment_fields = apply_filters( 'storeengine/payment_settings_fields', [] );
63
64 $this->actions['update_payments_settings']['fields']['payments'] = $this->payment_fields;
65 $this->actions['verify_payment_method_config']['fields']['config'] = array_merge( ...array_values( $this->payment_fields ) );
66 }
67
68 protected function load_fields() {
69 // The payment field schema is only consumed while handling this handler's
70 // wp_ajax_* actions (save / verify). Building it instantiates every payment
71 // gateway (whose setup() calls __()), so skip it off-ajax — this
72 // constructor runs on every request — and skip it before `init` to avoid
73 // loading translations too early. refresh_payment_fields() fills it in on
74 // `init` for ajax requests. See __construct().
75 $this->payment_fields = ( wp_doing_ajax() && did_action( 'init' ) ) ? apply_filters( 'storeengine/payment_settings_fields', [] ) : [];
76 $this->settings_fields = apply_filters( 'storeengine/ajax/settings_fields', [
77 'store_name' => 'string',
78 'store_email' => 'string',
79 'store_address_1' => 'string',
80 'store_address_2' => 'string',
81 'store_city' => 'string',
82 'store_state' => 'string',
83 'store_postcode' => 'string',
84 'store_country' => 'string',
85 'store_currency' => 'string',
86 'store_currency_position' => 'string',
87 'store_currency_thousand_separator' => 'string',
88 'store_currency_decimal_separator' => 'string',
89 'store_currency_decimal_limit' => 'integer',
90 // Brand & Style
91 'store_logo' => 'absint',
92 'global_primary_color' => 'hex_color',
93 'global_secondary_color' => 'hex_color',
94 'global_text_color' => 'hex_color',
95 'global_subtitle_color' => 'hex_color',
96 'global_input_text_color' => 'hex_color',
97 'global_border_color' => 'hex_color',
98 'global_background_color' => 'hex_color',
99 'global_placeholder_color' => 'hex_color',
100 // Products
101 'default_product_shipping_type' => 'string',
102 'enable_direct_checkout' => 'boolean',
103 'hide_quantity_selector' => 'boolean',
104 'hide_add_to_cart' => 'boolean',
105 'enable_product_reviews' => 'boolean',
106 'enable_product_comments' => 'boolean',
107 'review_permission' => 'string',
108 'review_media_max' => 'integer',
109 'review_approval' => 'string',
110 'enable_related_products' => 'boolean',
111 'enable_faqs' => 'boolean',
112 'faq_mode' => 'string',
113 'enable_product_tax' => 'boolean',
114 // SKU & Barcode auto-generation
115 'auto_generate_sku' => 'boolean',
116 'sku_pattern' => 'string',
117 'sku_number_padding' => 'integer',
118 'auto_generate_barcode' => 'boolean',
119 'barcode_ean_prefix' => 'string',
120 // Product Archive
121 'product_archive_sidebar_position' => 'string',
122 'product_archive_filters' => [
123 'search' => [
124 'status' => 'boolean',
125 'order' => 'integer',
126 ],
127 'category' => [
128 'status' => 'boolean',
129 'order' => 'integer',
130 ],
131 'tags' => [
132 'status' => 'boolean',
133 'order' => 'integer',
134 ],
135 ],
136 'product_archive_products_per_row' => [
137 'desktop' => 'integer',
138 'tablet' => 'integer',
139 'mobile' => 'integer',
140 ],
141 'product_archive_products_per_page' => 'integer',
142 'product_archive_products_order' => 'string',
143 'product_archive_multi_price_display' => 'string',
144 'product_single_price_display' => 'string',
145 'single_product_gallery_layout' => 'string',
146 'product_archive_card_carousel' => 'boolean',
147 'product_archive_card_swatches' => 'boolean',
148 'product_archive_quick_view' => 'boolean',
149 'quick_view_position' => 'string',
150 'quick_view_animation' => 'string',
151 'enable_recently_viewed' => 'boolean',
152 'enable_size_guide' => 'boolean',
153 'enable_wishlist' => 'boolean',
154 'enable_product_compare' => 'boolean',
155 // Pages
156 'shop_page' => 'integer',
157 'cart_page' => 'integer',
158 'checkout_page' => 'integer',
159 'thankyou_page' => 'integer',
160 'dashboard_page' => 'integer',
161 'membership_pricing_page' => 'integer',
162 'affiliate_registration_page' => 'integer',
163 // Geolocation
164 'maxmind_license' => 'password',
165 'default_customer_address' => 'string',
166 'enable_caching_support' => 'boolean',
167 // Selling / shipping country restrictions — conventional storefront parity.
168 // `allowed_countries`: 'all' | 'all_except' | 'specific'
169 // `ship_to_countries`: '' | 'all' | 'specific' | 'disabled' (empty = follow sell-to)
170 'allowed_countries' => 'string',
171 'all_except_countries' => 'array',
172 'specific_allowed_countries' => 'array',
173 'ship_to_countries' => 'string',
174 'specific_ship_to_countries' => 'array',
175 // Tax
176 'prices_include_tax' => 'boolean',
177 'tax_based_on' => 'string',
178 'shipping_tax_class' => 'string',
179 'tax_round_at_subtotal' => 'boolean',
180 'tax_classes' => 'string',
181 'tax_display_shop' => 'string',
182 'tax_display_cart' => 'string',
183 'price_display_suffix' => 'string',
184 'tax_total_display' => 'string',
185 'auth_redirect_type' => 'string',
186 'auth_redirect_url' => 'url',
187 'checkout_default_country' => 'country',
188 'enable_floating_cart' => 'boolean',
189 'auto_open_cart_drawer' => 'boolean',
190 'sticky_add_to_cart' => 'boolean',
191 'enable_after_purchase_redirect' => 'boolean',
192 'analytics' => [
193 'google' => 'boolean',
194 'facebook' => 'boolean',
195 ],
196 // Checkout Fields tab — saved as { field_id: { enabled: bool, required: bool } }.
197 // Declared as `array` so populate_field_data preserves the shape verbatim
198 // instead of stripping unknown sub-keys.
199 'checkout_fields' => 'array',
200 // Instant Checkout addon settings — same passthrough shape.
201 'instant_checkout' => 'array',
202 ] );
203 }
204
205 protected array $tax_total_display_options = [ 'single', 'itemized' ];
206 protected array $tax_display_options = [ 'incl', 'excl' ];
207 protected array $tax_base_options = [ 'shipping', 'billing', 'base' ];
208
209 protected array $archive_filter_options = [ 'search', 'category', 'tags' ];
210
211 protected function populate_field_data( array $fields, $payload = [], $defaults = [] ): array {
212 $output = [];
213
214 foreach ( $fields as $field => $type ) {
215 if ( is_array( $type ) ) {
216 $_defaults = array_key_exists( $field, $defaults ) ? $defaults[ $field ] : [];
217 $_payload = array_key_exists( $field, $payload ) ? $payload[ $field ] : $_defaults;
218 $_payload = null === $_payload || '' === $_payload ? [] : $_payload;
219 $output[ $field ] = $this->populate_field_data( $type, $_payload, $_defaults );
220 } else {
221 $output[ $field ] = $payload[ $field ] ?? ( $defaults[ $field ] ?? '' );
222 }
223 }
224
225 return $output;
226 }
227
228 protected function update_base_settings( $payload ) {
229 // Filling up blanks with saved data.
230 // Don't set from default data as it can reset saved data if field is unset.
231 // @XXX needs more testing.
232 // @see BaseSettings::get_settings_default_data
233 $default = BaseSettings::get_settings_saved_data();
234 // Set from default if not set and save the changes.
235
236 // Prepare filter widget settings.
237 $payload['product_archive_filters'] = $payload['product_archive_filters'] ?? ( $default['product_archive_filters'] ?? [] );
238
239 foreach ( $this->archive_filter_options as $option ) {
240 if ( empty( $payload['product_archive_filters'][ $option ] ) ) {
241 $payload['product_archive_filters'][ $option ] = [
242 'status' => false,
243 'order' => $default['product_archive_filters'][ $option ]['order'] ?? 0,
244 ];
245 } else {
246 $payload['product_archive_filters'][ $option ] = wp_parse_args( $payload['product_archive_filters'][ $option ], [
247 'status' => true,
248 'order' => 0,
249 ] );
250 }
251 }
252
253 // Validate global tax settings.
254 if ( ! empty( $payload['tax_total_display'] ) && ! in_array( $payload['tax_total_display'], $this->tax_total_display_options, true ) ) {
255 wp_send_json_error( __( 'Invalid cart & checkout tax total display option.', 'storeengine' ) );
256 }
257
258 if ( ! empty( $payload['tax_display_cart'] ) && ! in_array( $payload['tax_display_cart'], $this->tax_display_options, true ) ) {
259 wp_send_json_error( __( 'Invalid cart & checkout tax display option.', 'storeengine' ) );
260 }
261
262 if ( ! empty( $payload['tax_display_shop'] ) && ! in_array( $payload['tax_display_shop'], $this->tax_display_options, true ) ) {
263 wp_send_json_error( __( 'Invalid shop tax display option.', 'storeengine' ) );
264 }
265
266 if ( ! empty( $payload['tax_based_on'] ) && ! in_array( $payload['tax_based_on'], $this->tax_base_options, true ) ) {
267 wp_send_json_error( __( 'Invalid tax address base.', 'storeengine' ) );
268 }
269
270 $payload['auth_redirect_type'] = $payload['auth_redirect_type'] ?? ( $default['auth_redirect_type'] ?? 'storeengine' );
271 $payload['auth_redirect_url'] = $payload['auth_redirect_url'] ?? ( $default['auth_redirect_url'] ?? '' );
272
273 if ( ! in_array( $payload['auth_redirect_type'], [ 'default', 'storeengine', 'custom' ], true ) ) {
274 wp_send_json_error( __( 'Invalid dashboard login redirect.', 'storeengine' ) );
275 }
276
277 if ( 'custom' === $payload['auth_redirect_type'] ) {
278 if ( ! $payload['auth_redirect_url'] ) {
279 wp_send_json_error( __( 'Login URL is required.', 'storeengine' ) );
280 } else {
281 if ( filter_var( $payload['auth_redirect_url'], FILTER_VALIDATE_URL ) === false ) {
282 wp_send_json_error( __( 'Login URL is invalid.', 'storeengine' ) );
283 }
284
285 if ( is_ssl() && ! str_starts_with( $payload['auth_redirect_url'], 'https://' ) ) {
286 wp_send_json_error( __( 'Invalid dashboard login redirect URL. Please use secure (https) URL.', 'storeengine' ) );
287 }
288
289 if ( str_starts_with( $payload['auth_redirect_url'], Helper::get_dashboard_url() ) ) {
290 // Prevent redirect loop.
291 wp_send_json_error( __( 'Dashboard URL is not allowed. Please use StoreEngine as auth redirect instead.', 'storeengine' ) );
292 }
293
294 // Remove all allowed hosts except the site url.
295 remove_all_filters( 'allowed_redirect_hosts' );
296 if ( ! wp_validate_redirect( $payload['auth_redirect_url'] ) ) {
297 wp_send_json_error( __( 'Login URL is not allowed.', 'storeengine' ) );
298 }
299 }
300 }
301
302 // Prepare Order by.
303 $valid_orderby = [ 'menu_order', 'title', 'date', 'modified', 'ID' ];
304 $payload['product_archive_products_order'] = $payload['product_archive_products_order'] ?? ( $default['product_archive_products_order'] ?? '' );
305 $payload['product_archive_products_order'] = in_array( $payload['product_archive_products_order'], $valid_orderby, true ) ? $payload['product_archive_products_order'] : '';
306
307 if ( $payload['product_archive_multi_price_display'] && ! in_array( $payload['product_archive_multi_price_display'], [
308 'dropdown',
309 'price-range',
310 ], true ) ) {
311 wp_send_json_error( __( 'Invalid multi-price display settings.', 'storeengine' ) );
312 }
313
314 if ( ! empty( $payload['product_single_price_display'] ) && ! in_array( $payload['product_single_price_display'], [
315 'radio',
316 'dropdown',
317 ], true ) ) {
318 wp_send_json_error( __( 'Invalid single product price display settings.', 'storeengine' ) );
319 }
320
321 if ( ! empty( $payload['single_product_gallery_layout'] ) && ! in_array( $payload['single_product_gallery_layout'], [
322 'carousel',
323 'stacked',
324 'grid',
325 ], true ) ) {
326 wp_send_json_error( __( 'Invalid product gallery layout settings.', 'storeengine' ) );
327 }
328
329 if ( ! empty( $payload['quick_view_position'] ) && ! in_array( $payload['quick_view_position'], [
330 'center',
331 'left',
332 'right',
333 ], true ) ) {
334 wp_send_json_error( __( 'Invalid Quick View position setting.', 'storeengine' ) );
335 }
336
337 if ( ! empty( $payload['quick_view_animation'] ) && ! in_array( $payload['quick_view_animation'], [
338 'fade',
339 'zoom',
340 'slide',
341 'none',
342 ], true ) ) {
343 wp_send_json_error( __( 'Invalid Quick View animation setting.', 'storeengine' ) );
344 }
345
346 $errors = apply_filters( 'storeengine/ajax/validate_settings', new WP_Error(), $payload );
347
348 if ( is_wp_error( $errors ) && $errors->has_errors() ) {
349 wp_send_json_error( $errors, 400 );
350 }
351
352 $new_key = $payload['maxmind_license'] ?? '';
353 $saved_key = $default['maxmind_license'] ?? '';
354
355 if ( $new_key ) {
356 if ( $saved_key !== $new_key ) {
357 $is_valid = Geolocation::validate_maxmind_license_key( $new_key );
358 if ( is_wp_error( $is_valid ) ) {
359 wp_send_json_error( $is_valid->get_error_message() );
360 }
361
362 if ( ! file_exists( Geolocation::get_maxmind_db_path() ) ) {
363 \StoreEngine::init()->queue()->schedule_single( time() + 1, 'storeengine/geolocation/maxmind/db-update', [], 'storeengine' );
364 }
365 }
366 }
367
368 // Normalise checkout_fields. Form-encoded payloads send "false" as a literal
369 // string; coerce to real booleans so reads via (bool) return the correct value.
370 if ( isset( $payload['checkout_fields'] ) && is_array( $payload['checkout_fields'] ) ) {
371 $normalised = [];
372 foreach ( $payload['checkout_fields'] as $id => $row ) {
373 $row = is_array( $row ) ? $row : [];
374 $normalised[ sanitize_key( $id ) ] = [
375 'enabled' => CheckoutFields::to_bool( $row['enabled'] ?? false ),
376 'required' => CheckoutFields::to_bool( $row['required'] ?? false ),
377 ];
378 }
379 $payload['checkout_fields'] = $normalised;
380 }
381
382 // Prepare & save settings.
383 $is_update = BaseSettings::save_settings( $this->populate_field_data( $this->settings_fields, $payload, $default ) );
384
385 // Clear any unwanted data and flush rules.
386 Helper::flush_rewire_rules();
387
388 do_action( 'storeengine/admin/after_save_settings', $is_update, 'base', $payload );
389
390 wp_send_json_success( $is_update );
391 }
392
393
394 protected function update_payments_settings( $payload ) {
395 if ( empty( $payload['payments'] ) ) {
396 wp_send_json_error( esc_html__( 'Invalid request.', 'storeengine' ) );
397 }
398
399 foreach ( $payload['payments'] as $gateway => $data ) {
400 if ( ! array_key_exists( $gateway, $this->payment_fields ) ) {
401 continue;
402 }
403
404 do_action( 'storeengine/admin/save_gateways/' . $gateway . '/settings', $data );
405 }
406
407 wp_send_json_success( true );
408 }
409
410 /**
411 * Verify payment verification payload data.
412 *
413 * @param array $payload
414 *
415 * @throws StoreEngineException
416 */
417 protected function verify_payment_config_payload( array $payload ) {
418 if ( empty( $payload['method'] ) ) {
419 throw new StoreEngineException( esc_html__( 'payment method is required.', 'storeengine' ) );
420 }
421 if ( empty( $payload['config'] ) ) {
422 throw new StoreEngineException( esc_html__( 'Missing required fields.', 'storeengine' ) );
423 }
424
425 if ( ! array_key_exists( $payload['method'], $this->payment_fields ) ) {
426 throw new StoreEngineException( esc_html__( 'Payment method doesnt exists.', 'storeengine' ) );
427 }
428 }
429
430 protected function verify_payment_method_config( $payload ) {
431 try {
432 $this->verify_payment_config_payload( $payload );
433
434 $payment_method = $payload['method'];
435 do_action( "storeengine/admin/verify_payment_{$payment_method}_config", $payload['config'] );
436
437 wp_send_json_success();
438 } catch ( StoreEngineException $e ) {
439 /* translators: %s. Error message. */
440 wp_send_json_error( sprintf( esc_html__( 'Failed to verify payment method settings. Error: %s', 'storeengine' ), esc_html( $e->getMessage() ) ) );
441 }
442 }
443
444 protected function sanitize_bank_transfer_settings( $field_settings, int $index = 0 ): array {
445 return [
446 'type' => 'bank_transfer',
447 'is_enabled' => (bool) sanitize_text_field( $field_settings['is_enabled'] ),
448 'title' => sanitize_text_field( $field_settings['title'] ),
449 'description' => sanitize_text_field( $field_settings['description'] ),
450 'instructions' => sanitize_text_field( $field_settings['instructions'] ),
451 'accounts' => [],
452 'index' => $index,
453 ];
454 }
455
456 protected function sanitize_check_payment_settings( $field_settings, int $index = 0 ): array {
457 return [
458 'type' => 'check_payment',
459 'is_enabled' => (bool) sanitize_text_field( $field_settings['is_enabled'] ),
460 'title' => sanitize_text_field( $field_settings['title'] ),
461 'description' => sanitize_text_field( $field_settings['description'] ),
462 'instructions' => sanitize_text_field( $field_settings['instructions'] ),
463 'index' => $index,
464 ];
465 }
466
467 protected function sanitize_cash_on_delivery_settings( $field_settings, int $index = 0 ): array {
468 return [
469 'type' => 'cash_on_delivery',
470 'is_enabled' => (bool) sanitize_text_field( $field_settings['is_enabled'] ),
471 'title' => sanitize_text_field( $field_settings['title'] ),
472 'description' => sanitize_text_field( $field_settings['description'] ),
473 'instructions' => sanitize_text_field( $field_settings['instructions'] ),
474 'index' => $index,
475 ];
476 }
477
478 protected function sanitize_paypal_settings( $field_settings, int $index = 0 ): array {
479 return [
480 'type' => 'paypal',
481 'is_enabled' => (bool) sanitize_text_field( $field_settings['is_enabled'] ),
482 'is_enabled_sandbox' => (bool) sanitize_text_field( $field_settings['is_enabled_sandbox'] ),
483 'sandbox_client_id' => sanitize_text_field( $field_settings['sandbox_client_id'] ),
484 'sandbox_client_secret' => sanitize_text_field( $field_settings['sandbox_client_secret'] ),
485 'live_client_id' => sanitize_text_field( $field_settings['live_client_id'] ),
486 'live_client_secret' => sanitize_text_field( $field_settings['live_client_secret'] ),
487 'index' => $index,
488 ];
489 }
490
491 protected function sanitize_stripe_settings( $field_settings, int $index = 0 ): array {
492 return [
493 'type' => 'stripe',
494 'is_enabled' => (bool) sanitize_text_field( $field_settings['is_enabled'] ),
495 'is_enabled_test_mode' => (bool) sanitize_text_field( $field_settings['is_enabled_test_mode'] ),
496 'test_publishable_key' => sanitize_text_field( $field_settings['test_publishable_key'] ),
497 'test_secret_key' => sanitize_text_field( $field_settings['test_secret_key'] ),
498 'live_publishable_key' => sanitize_text_field( $field_settings['live_publishable_key'] ),
499 'live_secret_key' => sanitize_text_field( $field_settings['live_secret_key'] ),
500 'index' => $index,
501 ];
502 }
503 }
504