PluginProbe
seQura / 4.3.3
seQura v4.3.3
4.3.4 4.3.3 4.3.2 4.3.1 trunk 2.0.0 2.0.10 2.0.11 2.0.12 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 3.0.0 3.0.2 3.0.5 3.0.6 3.0.7 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 4.0.0 All 30 releases
sequra / src / Controllers / Hooks / Product / class-product-controller.php

class-product-controller.php in seQura 4.3.3, at src/Controllers/Hooks/Product/class-product-controller.php

444 lines 15.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Product Controller
4 *
5 * @package SeQura/WC
6 * @subpackage SeQura/WC/Controllers
7 */
8
9 namespace SeQura\WC\Controllers\Hooks\Product;
10
11 use SeQura\Core\Infrastructure\Logger\LogContextData;
12 use SeQura\Core\Infrastructure\Utility\RegexProvider;
13 use SeQura\WC\Controllers\Controller;
14 use SeQura\WC\Services\Log\Interface_Logger_Service;
15 use SeQura\WC\Services\Product\Interface_Product_Service;
16 use SeQura\WC\Services\Widgets\Interface_Widgets_Service;
17 use WC_Product;
18 use WP_Post;
19
20 /**
21 * Product Controller implementation
22 *
23 * @phpstan-import-type WidgetDataArray from Interface_Widgets_Service
24 */
25 class Product_Controller extends Controller implements Interface_Product_Controller {
26
27 private const FIELD_NAME_IS_BANNED = 'is_sequra_banned';
28 private const FIELD_NAME_IS_SERVICE = 'is_sequra_service';
29 private const FIELD_NAME_SERVICE_END_DATE = 'sequra_service_end_date';
30 private const FIELD_NAME_SERVICE_DESIRED_FIRST_CHARGE_DATE = 'sequra_desired_first_charge_date';
31 private const FIELD_NAME_REGISTRATION_AMOUNT = 'sequra_registration_amount';
32 private const NONCE_SEQURA_PRODUCT = '_sequra_product_nonce';
33
34 /**
35 * Product service
36 *
37 * @var Interface_Product_Service
38 */
39 private $product_service;
40
41 /**
42 * RegEx service
43 *
44 * @var RegexProvider
45 */
46 private $regex;
47
48 /**
49 * Cached available widgets for product page
50 *
51 * @var array<WidgetDataArray[]>
52 */
53 private $available_widgets_for_product_page;
54
55 /**
56 * Widgets service
57 *
58 * @var Interface_Widgets_Service
59 */
60 private $widgets_service;
61
62 /**
63 * Constructor
64 */
65 public function __construct(
66 Interface_Logger_Service $logger,
67 string $templates_path,
68 Interface_Product_Service $product_service,
69 RegexProvider $regex,
70 Interface_Widgets_Service $widgets_service
71 ) {
72 parent::__construct( $logger, $templates_path );
73 $this->logger = $logger;
74 $this->product_service = $product_service;
75 $this->regex = $regex;
76 $this->widgets_service = $widgets_service;
77 $this->available_widgets_for_product_page = array();
78 }
79
80 /**
81 * Handle the widget shortcode callback
82 * [sequra_widget]
83 * Expected attributes:
84 * - product: The seQura product identifier. Required.
85 * - campaign: The seQura campaign name. Optional.
86 * - dest: A CSS selector to place the widget. Optional. If not provided, the widget will be placed in the same container as the shortcode.
87 * - product_id: The WooCommerce product identifier. Optional.
88 * - price: A CSS selector to get the product price or the price itself as a string. Optional.
89 * - alt_price: An alternative CSS selector to retrieve the product price for special product page layouts. Optional.
90 * - is_alt_price: A CSS selector to determine if the product has an alternative price. Optional.
91 * - reg_amount: The registration amount. Optional.
92 * - theme: The theme to use. Accepted values are: L, R, legacy, legacyL, legacyR, minimal, minimalL, minimalR or JSON formatted string. Optional.
93 * - min_amount: The minimum amount to display the widget. Optional.
94 * - max_amount: The maximum amount to display the widget. Optional.
95 *
96 * @param array<string, string> $atts The shortcode attributes
97 * @return string
98 */
99 public function do_widget_shortcode( $atts ) {
100 $this->logger->log_debug( 'Shortcode called', __FUNCTION__, __CLASS__ );
101 $atts = (array) $atts;
102
103 // Check for required attributes.
104 foreach ( array( 'product' ) as $required ) {
105 if ( ! isset( $atts[ $required ] ) ) {
106 $this->logger->log_error( "\"$required\" attribute is required", __FUNCTION__, __CLASS__ );
107 return '';
108 }
109 }
110
111 // Decode HTML entities in attributes if any.
112 foreach ( $atts as $key => $value ) {
113 $atts[ $key ] = html_entity_decode( $value, ENT_QUOTES );
114 }
115
116 $product_id = intval( $atts['product_id'] ?? 0 );
117 $product = $atts['product'];
118 $campaign = isset( $atts['campaign'] ) && '' !== $atts['campaign'] ? $atts['campaign'] : '';
119 $widget = $this->get_widget_for_product( $product_id, $product, $campaign );
120 if ( ! $widget ) {
121 $this->logger->log_info(
122 'Widget is disabled',
123 __FUNCTION__,
124 __CLASS__,
125 array(
126 new LogContextData( 'payment_method', $product ),
127 new LogContextData( 'campaign', $campaign ),
128 new LogContextData( 'product_id', $product_id ),
129 )
130 );
131 return '';
132 }
133
134 if ( ! $this->product_service->can_display_widget_for_method( $product_id, $product ) ) {
135 $this->logger->log_info(
136 'Widget cannot be displayed for product',
137 __FUNCTION__,
138 __CLASS__,
139 array(
140 new LogContextData( 'payment_method', $product ),
141 new LogContextData( 'campaign', $campaign ),
142 new LogContextData( 'product_id', $product_id ),
143 )
144 );
145 return '';
146 }
147
148 // Replace old attribute names introduced in 2.0.0 with the new ones.
149 $atts_replacements = array(
150 'variation_price' => 'alt_price',
151 'is_variable' => 'is_alt_price',
152 );
153
154 foreach ( $atts_replacements as $old => $new ) {
155 if ( isset( $atts[ $old ] ) ) {
156 $atts[ $new ] = $atts[ $old ];
157 unset( $atts[ $old ] );
158 }
159 }
160
161 $atts = \shortcode_atts(
162 array(
163 'product' => $widget['product'] ?? '',
164 'campaign' => $widget['campaign'] ?? '',
165 'theme' => $widget['theme'] ?? '',
166 'reverse' => $widget['reverse'] ?? '0',
167 'reg_amount' => $this->product_service->get_registration_amount( $product_id, true ),
168 'dest' => '',
169 'price' => $widget['priceSel'] ?? '',
170 'alt_price' => $widget['altPriceSel'] ?? '',
171 'is_alt_price' => $widget['altTriggerSelector'] ?? '',
172 'min_amount' => $widget['minAmount'] ?? 0,
173 'max_amount' => $widget['maxAmount'] ?? null,
174 ),
175 $atts,
176 'sequra_widget'
177 );
178
179 ob_start();
180 \wc_get_template( 'front/widget.php', $atts, '', $this->templates_path );
181 return ob_get_clean();
182 }
183
184 /**
185 * Handle the cart widget shortcode callback
186 *
187 * @param array<string, string> $atts The shortcode attributes
188 * @return string
189 */
190 public function do_cart_widget_shortcode( $atts ) {
191 $this->logger->log_debug( 'Shortcode called', __FUNCTION__, __CLASS__ );
192
193 $widget = $this->widgets_service->get_widget_for_cart_page();
194 if ( ! $widget ) {
195 return '';
196 }
197
198 $atts = \shortcode_atts(
199 array(
200 'product' => $widget['product'] ?? '',
201 'campaign' => $widget['campaign'] ?? '',
202 'dest' => $widget['dest'] ?? '',
203 'theme' => $widget['theme'] ?? '',
204 'price' => $widget['priceSel'] ?? '',
205 'alt_price' => $widget['altPriceSel'] ?? '',
206 'is_alt_price' => $widget['altTriggerSelector'] ?? '',
207 'message' => $widget['miniWidgetMessage'],
208 'message_below_limit' => $widget['miniWidgetBelowLimitMessage'],
209 'min_amount' => $widget['minAmount'] ?? 0,
210 'max_amount' => $widget['maxAmount'] ?? null,
211 'reg_amount' => 0,
212 'reverse' => 0,
213 ),
214 (array) $atts,
215 'sequra_cart_widget'
216 );
217
218 ob_start();
219 \wc_get_template( 'front/widget.php', $atts, '', $this->templates_path );
220 return ob_get_clean();
221 }
222
223 /**
224 * Handle the product listing widget shortcode callback
225 *
226 * @param array<string, string> $atts The shortcode attributes
227 * @return string
228 */
229 public function do_product_listing_widget_shortcode( $atts ) {
230 $this->logger->log_debug( 'Shortcode called', __FUNCTION__, __CLASS__ );
231 $widget = $this->widgets_service->get_widget_for_product_listing_page();
232 if ( ! $widget ) {
233 return '';
234 }
235
236 $atts = \shortcode_atts(
237 array(
238 'product' => $widget['product'] ?? '',
239 'campaign' => $widget['campaign'] ?? '',
240 'dest' => $widget['dest'],
241 'price' => $widget['priceSel'],
242 'message' => $widget['miniWidgetMessage'],
243 'message_below_limit' => $widget['miniWidgetBelowLimitMessage'],
244 'min_amount' => $widget['minAmount'] ?? 0,
245 'max_amount' => $widget['maxAmount'] ?? null,
246 ),
247 (array) $atts,
248 'sequra_product_listing_widget'
249 );
250
251 ob_start();
252 \wc_get_template( 'front/mini_widget.php', $atts, '', $this->templates_path );
253 return ob_get_clean();
254 }
255
256 /**
257 * Get available widgets for a product page from in-memory cache or CheckoutAPI
258 *
259 * @param int|string $product_id The product ID
260 * @return WidgetDataArray[] The available widgets
261 */
262 private function get_available_widgets_for_product_page( $product_id ): array {
263
264 $product_id = (string) $product_id;
265 if ( isset( $this->available_widgets_for_product_page[ $product_id ] ) ) {
266 return $this->available_widgets_for_product_page[ $product_id ];
267 }
268
269 $widgets = $this->widgets_service->get_widgets_for_product_page( $product_id );
270
271 if ( ! $widgets ) {
272 return array();
273 }
274
275 $this->available_widgets_for_product_page[ $product_id ] = $widgets;
276 return $widgets;
277 }
278
279 /**
280 * Get the widget for a product if enabled
281 *
282 * @param string|int $product_id The WooCommerce product ID. Pass and empty value to ignore it.
283 * @param string $product The seQura product identifier
284 * @param string $campaign The seQura campaign name. Use an empty string to ignore the campaign.
285 *
286 * @return WidgetDataArray|null The widget data if enabled, null otherwise
287 */
288 private function get_widget_for_product( $product_id, string $product, string $campaign = '' ): ?array {
289 /**
290 * Promotional widget data
291 *
292 * @var array<string, mixed> $widget */
293 foreach ( $this->get_available_widgets_for_product_page( $product_id ) as $widget ) {
294 if ( $widget['product'] === $product && $widget['campaign'] === $campaign ) {
295 return $widget;
296 }
297 }
298 return null;
299 }
300
301 /**
302 * Add [sequra_widget] to product page automatically
303 */
304 private function add_widget_shortcode_to_product_page(): void {
305 if ( ! \is_product() ) {
306 return;
307 }
308 /**
309 * The current product.
310 *
311 * @var WC_Product $product
312 */
313 global $product;
314
315 foreach ( $this->get_available_widgets_for_product_page( $product->get_id() ) as $widget ) {
316 $atts = array(
317 'product' => $widget['product'] ?? '',
318 'campaign' => $widget['campaign'] ?? '',
319 'product_id' => $product->get_id(),
320 'theme' => $widget['theme'] ?? '',
321 'dest' => $widget['dest'] ?? '',
322 'price' => $widget['priceSel'] ?? '',
323 'alt_price' => $widget['altPriceSel'] ?? '',
324 'is_alt_price' => $widget['altTriggerSelector'] ?? '',
325 'min_amount' => $widget['minAmount'] ?? 0,
326 'max_amount' => $widget['maxAmount'] ?? null,
327
328 );
329 $atts_str = '';
330 foreach ( $atts as $key => $value ) {
331 if ( \is_string( $value ) ) {
332 // Escape special characters to avoid breaking the shortcode.
333 $value = \str_replace( array( '[', ']', "'" ), array( '&#91;', '&#93;', '&#39;' ), $value );
334 }
335
336 $atts_str .= " $key='$value'";
337 }
338 echo \do_shortcode( "[sequra_widget $atts_str]" );
339 }
340 }
341
342 /**
343 * Add [sequra_cart_widget] to product page automatically
344 */
345 private function add_widget_shortcode_to_cart_page(): void {
346 if ( ! \is_cart() ) {
347 return;
348 }
349 echo \do_shortcode( '[sequra_cart_widget]' );
350 }
351
352 /**
353 * Add [sequra_product_listing_widget] to product archive automatically
354 */
355 private function add_widget_shortcode_to_product_listing_page(): void {
356 if ( ! \is_product_category() && ! \is_product_tag() && ! \is_shop() ) {
357 return;
358 }
359 echo \do_shortcode( '[sequra_product_listing_widget]' );
360 }
361
362 /**
363 * Add [sequra_widget] to product page automatically
364 * Add [sequra_cart_widget] to cart page automatically
365 *
366 * @return void
367 */
368 public function add_widget_shortcode_to_page() {
369 $this->logger->log_debug( 'Hook executed', __FUNCTION__, __CLASS__ );
370 if ( \did_action( 'before_woocommerce_sequra_add_widget_to_page' ) ) {
371 return;
372 }
373
374 /**
375 * Fires before the SeQura widget is added to the page to prevent multiple executions.
376 *
377 * @since 3.0.0
378 */
379 \do_action( 'before_woocommerce_sequra_add_widget_to_page' );
380
381 $this->add_widget_shortcode_to_product_page();
382 $this->add_widget_shortcode_to_cart_page();
383 $this->add_widget_shortcode_to_product_listing_page();
384 }
385
386 /**
387 * Add meta boxes to the product edit page
388 *
389 * @return void
390 */
391 public function add_meta_boxes() {
392 \add_meta_box( 'sequra_settings', esc_html__( 'seQura settings', 'sequra' ), array( $this, 'render_meta_boxes' ), 'product', 'side', 'default' );
393 }
394
395 /**
396 * Render the meta boxes
397 *
398 * @param WP_Post $post The post object
399 * @return void
400 */
401 public function render_meta_boxes( $post ) {
402 $args = array(
403 'is_banned' => $this->product_service->get_is_banned( $post->ID ),
404 'is_banned_field_name' => self::FIELD_NAME_IS_BANNED,
405 'enabled_for_services' => $this->product_service->is_enabled_for_services(),
406 'is_service' => $this->product_service->is_service( $post->ID ),
407 'is_service_field_name' => self::FIELD_NAME_IS_SERVICE,
408 'service_end_date_default' => $this->product_service->get_default_services_end_date(),
409 'service_end_date' => $this->product_service->get_service_end_date( $post->ID, true ),
410 'service_end_date_field_name' => self::FIELD_NAME_SERVICE_END_DATE,
411 'allow_payment_delay' => $this->product_service->is_allow_first_service_payment_delay(),
412 'service_desired_first_charge_date' => $this->product_service->get_desired_first_charge_date( $post->ID, true ) ?? '',
413 'service_desired_first_charge_date_field_name' => self::FIELD_NAME_SERVICE_DESIRED_FIRST_CHARGE_DATE,
414 'date_or_duration_regex' => $this->regex->getDateOrDurationRegex( false ),
415 'allow_registration_items' => $this->product_service->is_allow_service_registration_items(),
416 'service_registration_amount' => $this->product_service->get_registration_amount( $post->ID ),
417 'service_registration_amount_field_name' => self::FIELD_NAME_REGISTRATION_AMOUNT,
418 'nonce_name' => self::NONCE_SEQURA_PRODUCT,
419 );
420 \wc_get_template( 'admin/product_metabox.php', $args, '', $this->templates_path );
421 }
422
423 /**
424 * Save product meta
425 *
426 * @param int $post_id The post ID
427 * @return void
428 */
429 public function save_product_meta( $post_id ) {
430 if ( ! isset( $_POST[ self::NONCE_SEQURA_PRODUCT ] )
431 || ! \wp_verify_nonce( $_POST[ self::NONCE_SEQURA_PRODUCT ], -1 ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
432 || ! \current_user_can( 'edit_post', $post_id )
433 || \wp_doing_ajax() ) {
434 return;
435 }
436
437 $this->product_service->set_is_banned( $post_id, isset( $_POST[ self::FIELD_NAME_IS_BANNED ] ) ? \sanitize_text_field( \wp_unslash( $_POST[ self::FIELD_NAME_IS_BANNED ] ) ) : null );
438 $this->product_service->set_is_service( $post_id, isset( $_POST[ self::FIELD_NAME_IS_SERVICE ] ) ? \sanitize_text_field( \wp_unslash( $_POST[ self::FIELD_NAME_IS_SERVICE ] ) ) : null );
439 $this->product_service->set_service_end_date( $post_id, ! empty( $_POST[ self::FIELD_NAME_SERVICE_END_DATE ] ) ? \sanitize_text_field( \wp_unslash( $_POST[ self::FIELD_NAME_SERVICE_END_DATE ] ) ) : null );
440 $this->product_service->set_desired_first_charge_date( $post_id, ! empty( $_POST[ self::FIELD_NAME_SERVICE_DESIRED_FIRST_CHARGE_DATE ] ) ? \sanitize_text_field( \wp_unslash( $_POST[ self::FIELD_NAME_SERVICE_DESIRED_FIRST_CHARGE_DATE ] ) ) : null );
441 $this->product_service->set_registration_amount( $post_id, ! empty( $_POST[ self::FIELD_NAME_REGISTRATION_AMOUNT ] ) ? \sanitize_text_field( \wp_unslash( $_POST[ self::FIELD_NAME_REGISTRATION_AMOUNT ] ) ) : null );
442 }
443 }
444