PluginProbe
bBlocks – Essential Gutenberg Blocks & Patterns Collection / 2.1.8
bBlocks – Essential Gutenberg Blocks & Patterns Collection v2.1.8
2.1.9 2.1.8 2.1.7 2.1.6 2.1.5 2.1.4 2.1.3 2.1.2 2.1.1 2.1.0 2.0.43 2.0.42 2.0.41 2.0.40 2.0.39 2.0.38 trunk 1.0 1.1 1.2 1.3 1.4 1.5 1.5.1 1.5.2 All 109 releases
b-blocks / includes / blocks / woo-featured-product / WooFeaturedProduct.php

WooFeaturedProduct.php in bBlocks – Essential Gutenberg Blocks & Patterns Collection 2.1.8, at includes/blocks/woo-featured-product/WooFeaturedProduct.php

247 lines 11.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Woo Featured Product — shared server logic.
4 *
5 * Provides attribute sanitization and a hardened add-to-cart AJAX endpoint
6 * (`bb_wfp_add_to_cart`) used by the frontend `view.js` for simple products.
7 *
8 * Security model for `bb_wfp_add_to_cart`:
9 * - Nonce verified on every request via check_ajax_referer().
10 * - product_id sanitized with absint() and validated against wc_get_product().
11 * - Only purchasable, in-stock, simple products are added.
12 * - All responses use wp_send_json_success / wp_send_json_error.
13 *
14 * @package bBlocks
15 */
16
17 namespace BBlocks\Inc\Blocks;
18
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit;
21 }
22
23 class WooFeaturedProduct {
24
25 /**
26 * Allowed aspect ratios.
27 *
28 * @var string[]
29 */
30 const RATIOS = [ '1/1', '4/3', '3/4', '16/9' ];
31
32 /**
33 * Hook the AJAX endpoint (public + logged-in).
34 */
35 public function __construct() {
36 add_action( 'wp_ajax_bb_wfp_add_to_cart', [ $this, 'ajaxAddToCart' ] );
37 add_action( 'wp_ajax_nopriv_bb_wfp_add_to_cart', [ $this, 'ajaxAddToCart' ] );
38 }
39
40 /* ----------------------------------------------------------------------
41 * Sanitizers
42 * ------------------------------------------------------------------- */
43
44 /**
45 * Sanitize a CSS color value (hex, rgb/hsl, var(), or a CSS keyword).
46 *
47 * @param mixed $color Raw color.
48 * @param string $fallback Fallback when invalid.
49 * @return string
50 */
51 public static function sanitizeColor( $color, $fallback = '' ) {
52 $color = trim( (string) $color );
53 if ( '' === $color ) {
54 return $fallback;
55 }
56 if ( preg_match( '/^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/', $color ) ) {
57 return $color;
58 }
59 if ( preg_match( '/^(rgb|rgba|hsl|hsla)\s*\([0-9\s,%.\/]+\)$/i', $color ) ) {
60 return $color;
61 }
62 if ( preg_match( '/^var\(\s*--[a-zA-Z0-9\-_]+\s*(,\s*[a-zA-Z0-9 #%.,\-_\/]+)?\s*\)$/', $color ) ) {
63 return $color;
64 }
65 if ( preg_match( '/^[a-zA-Z]{1,30}$/', $color ) ) {
66 return $color;
67 }
68 return $fallback;
69 }
70
71 /**
72 * Clamp a value to an integer range.
73 *
74 * @param mixed $value Raw value.
75 * @param int $min Minimum.
76 * @param int $max Maximum.
77 * @param int $fallback Fallback when non-numeric.
78 * @return int
79 */
80 public static function clampInt( $value, $min, $max, $fallback ) {
81 if ( ! is_numeric( $value ) ) {
82 return (int) $fallback;
83 }
84 $value = (int) $value;
85 if ( $value < $min ) {
86 return (int) $min;
87 }
88 if ( $value > $max ) {
89 return (int) $max;
90 }
91 return $value;
92 }
93
94 /**
95 * Pick a value from an allowlist.
96 *
97 * @param mixed $value Raw value.
98 * @param string[] $allowed Allowed values.
99 * @param string $fallback Fallback.
100 * @return string
101 */
102 public static function pickFrom( $value, array $allowed, $fallback ) {
103 $value = is_string( $value ) ? trim( $value ) : '';
104 return in_array( $value, $allowed, true ) ? $value : $fallback;
105 }
106
107 /**
108 * Extract a clamped pixel integer from a CSS size string.
109 *
110 * @param mixed $value Raw value (e.g. "28px").
111 * @param int $min Minimum.
112 * @param int $max Maximum.
113 * @param int $fallback Fallback.
114 * @return int
115 */
116 public static function sizeInt( $value, $min, $max, $fallback ) {
117 $num = preg_replace( '/[^0-9]/', '', (string) $value );
118 return self::clampInt( '' === $num ? $fallback : $num, $min, $max, $fallback );
119 }
120
121 /**
122 * Normalize and sanitize the full attribute set into a safe, typed array.
123 *
124 * @param array $attributes Raw block attributes.
125 * @return array
126 */
127 public static function resolveAttributes( array $attributes ) {
128 $titleFont = (array) ( $attributes['titleFontSize'] ?? [] );
129 $descFont = (array) ( $attributes['descriptionFontSize'] ?? [] );
130 $btnFont = (array) ( $attributes['btnFontSize'] ?? [] );
131
132 $saleBadgeLabel = isset( $attributes['saleBadgeLabel'] ) ? wp_strip_all_tags( (string) $attributes['saleBadgeLabel'] ) : '';
133 $saleBadgeLabel = '' !== trim( $saleBadgeLabel ) ? $saleBadgeLabel : __( 'Sale', 'b-blocks' );
134
135 $addToCartLabel = isset( $attributes['addToCartLabel'] ) ? wp_strip_all_tags( (string) $attributes['addToCartLabel'] ) : '';
136 $addToCartLabel = '' !== trim( $addToCartLabel ) ? $addToCartLabel : __( 'Add to Cart', 'b-blocks' );
137
138 $viewProductLabel = isset( $attributes['viewProductLabel'] ) ? wp_strip_all_tags( (string) $attributes['viewProductLabel'] ) : '';
139 $viewProductLabel = '' !== trim( $viewProductLabel ) ? $viewProductLabel : __( 'View Product', 'b-blocks' );
140
141 $noProductMessage = isset( $attributes['noProductMessage'] ) ? wp_strip_all_tags( (string) $attributes['noProductMessage'] ) : '';
142 $noProductMessage = '' !== trim( $noProductMessage ) ? $noProductMessage : __( 'Please select a product.', 'b-blocks' );
143
144 return [
145 'productId' => self::clampInt( $attributes['productId'] ?? 0, 0, PHP_INT_MAX, 0 ),
146 'layout' => self::pickFrom( $attributes['layout'] ?? 'horizontal', [ 'horizontal', 'vertical' ], 'horizontal' ),
147 'imagePosition' => self::pickFrom( $attributes['imagePosition'] ?? 'left', [ 'left', 'right' ], 'left' ),
148 'imageSplit' => self::clampInt( $attributes['imageSplit'] ?? 50, 30, 70, 50 ),
149 'imageRatio' => self::pickFrom( $attributes['imageRatio'] ?? '1/1', self::RATIOS, '1/1' ),
150 'imageFit' => self::pickFrom( $attributes['imageFit'] ?? 'cover', [ 'cover', 'contain' ], 'cover' ),
151 'imageBorderRadius' => self::clampInt( $attributes['imageBorderRadius'] ?? 8, 0, 48, 8 ),
152 'showImage' => ! isset( $attributes['showImage'] ) || (bool) $attributes['showImage'],
153 'showBadge' => ! isset( $attributes['showBadge'] ) || (bool) $attributes['showBadge'],
154 'saleBadgeLabel' => $saleBadgeLabel,
155 'showRating' => ! isset( $attributes['showRating'] ) || (bool) $attributes['showRating'],
156 'showPrice' => ! isset( $attributes['showPrice'] ) || (bool) $attributes['showPrice'],
157 'showShortDescription' => ! isset( $attributes['showShortDescription'] ) || (bool) $attributes['showShortDescription'],
158 'showAddToCart' => ! isset( $attributes['showAddToCart'] ) || (bool) $attributes['showAddToCart'],
159 'addToCartLabel' => $addToCartLabel,
160 'viewProductLabel' => $viewProductLabel,
161 'contentAlign' => self::pickFrom( $attributes['contentAlign'] ?? 'left', [ 'left', 'center', 'right' ], 'left' ),
162 'contentVerticalAlign' => self::pickFrom( $attributes['contentVerticalAlign'] ?? 'center', [ 'top', 'center', 'bottom' ], 'center' ),
163 'gap' => self::clampInt( $attributes['gap'] ?? 32, 0, 80, 32 ),
164 'maxWidth' => self::clampInt( $attributes['maxWidth'] ?? 0, 0, 1600, 0 ),
165 'blockAlign' => self::pickFrom( $attributes['blockAlign'] ?? 'center', [ 'left', 'center', 'right' ], 'center' ),
166 'blockPaddingVertical' => self::clampInt( $attributes['blockPaddingVertical'] ?? 40, 0, 120, 40 ),
167 'blockPaddingHorizontal' => self::clampInt( $attributes['blockPaddingHorizontal'] ?? 40, 0, 120, 40 ),
168 'blockBG' => self::sanitizeColor( $attributes['blockBG'] ?? '', 'transparent' ),
169 'blockBorderRadius' => self::clampInt( $attributes['blockBorderRadius'] ?? 0, 0, 48, 0 ),
170 'titleColor' => self::sanitizeColor( $attributes['titleColor'] ?? '', 'inherit' ),
171 'titleSizeDesktop' => self::sizeInt( $titleFont['desktop'] ?? '28', 12, 80, 28 ),
172 'titleSizeTablet' => self::sizeInt( $titleFont['tablet'] ?? '24', 12, 72, 24 ),
173 'titleSizeMobile' => self::sizeInt( $titleFont['mobile'] ?? '20', 12, 64, 20 ),
174 'titleFontWeight' => self::clampInt( $attributes['titleFontWeight'] ?? 700, 400, 900, 700 ),
175 'priceColor' => self::sanitizeColor( $attributes['priceColor'] ?? '', '#e44d3a' ),
176 'regularPriceColor' => self::sanitizeColor( $attributes['regularPriceColor'] ?? '', '#999999' ),
177 'ratingColor' => self::sanitizeColor( $attributes['ratingColor'] ?? '', '#f5a623' ),
178 'descriptionColor' => self::sanitizeColor( $attributes['descriptionColor'] ?? '', 'inherit' ),
179 'descSizeDesktop' => self::sizeInt( $descFont['desktop'] ?? '15', 10, 40, 15 ),
180 'descSizeTablet' => self::sizeInt( $descFont['tablet'] ?? '14', 10, 36, 14 ),
181 'descSizeMobile' => self::sizeInt( $descFont['mobile'] ?? '14', 10, 32, 14 ),
182 'badgeBG' => self::sanitizeColor( $attributes['badgeBG'] ?? '', '#e44d3a' ),
183 'badgeTextColor' => self::sanitizeColor( $attributes['badgeTextColor'] ?? '', '#ffffff' ),
184 'btnColor' => self::sanitizeColor( $attributes['btnColor'] ?? '', '#ffffff' ),
185 'btnBG' => self::sanitizeColor( $attributes['btnBG'] ?? '', '#146EF5' ),
186 'btnHovColor' => self::sanitizeColor( $attributes['btnHovColor'] ?? '', '#ffffff' ),
187 'btnHovBG' => self::sanitizeColor( $attributes['btnHovBG'] ?? '', '#070127' ),
188 'btnRadius' => self::clampInt( $attributes['btnRadius'] ?? 6, 0, 48, 6 ),
189 'btnSizeDesktop' => self::sizeInt( $btnFont['desktop'] ?? '16', 10, 40, 16 ),
190 'btnSizeTablet' => self::sizeInt( $btnFont['tablet'] ?? '15', 10, 36, 15 ),
191 'btnSizeMobile' => self::sizeInt( $btnFont['mobile'] ?? '14', 10, 32, 14 ),
192 'btnFontWeight' => self::clampInt( $attributes['btnFontWeight'] ?? 600, 400, 900, 600 ),
193 'btnPaddingVertical' => self::clampInt( $attributes['btnPaddingVertical'] ?? 12, 4, 40, 12 ),
194 'btnPaddingHorizontal' => self::clampInt( $attributes['btnPaddingHorizontal'] ?? 28, 8, 80, 28 ),
195 'mobileMirror' => ! empty( $attributes['mobileMirror'] ),
196 'noProductMessage' => $noProductMessage,
197 ];
198 }
199
200 /* ----------------------------------------------------------------------
201 * AJAX endpoint
202 * ------------------------------------------------------------------- */
203
204 /**
205 * Handle the `bb_wfp_add_to_cart` AJAX request for simple products.
206 *
207 * Returns JSON: { added: true, productName } or an error.
208 */
209 public function ajaxAddToCart() {
210 check_ajax_referer( 'bb_wfp_add_to_cart', 'nonce' );
211
212 if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
213 wp_send_json_error( [ 'message' => __( 'WooCommerce is not available.', 'b-blocks' ) ] );
214 }
215
216 $productId = isset( $_POST['product_id'] ) ? absint( wp_unslash( $_POST['product_id'] ) ) : 0;
217 if ( $productId < 1 ) {
218 wp_send_json_error( [ 'message' => __( 'Invalid product.', 'b-blocks' ) ] );
219 }
220
221 $product = wc_get_product( $productId );
222 if ( ! $product || ! is_a( $product, 'WC_Product' ) ) {
223 wp_send_json_error( [ 'message' => __( 'Product not found.', 'b-blocks' ) ] );
224 }
225
226 if ( ! $product->is_type( 'simple' ) || ! $product->is_purchasable() || ! $product->is_in_stock() ) {
227 wp_send_json_error( [ 'message' => __( 'This product cannot be added to the cart.', 'b-blocks' ) ] );
228 }
229
230 $added = WC()->cart->add_to_cart( $productId, 1 );
231
232 if ( ! $added ) {
233 wp_send_json_error( [ 'message' => __( 'Could not add the product to the cart.', 'b-blocks' ) ] );
234 }
235
236 wp_send_json_success(
237 [
238 'added' => true,
239 'productName' => wp_strip_all_tags( $product->get_name() ),
240 'cartCount' => WC()->cart->get_cart_contents_count(),
241 ]
242 );
243 }
244 }
245
246 new WooFeaturedProduct();
247